summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-20 21:38:40 +0200
committerPaul Buetow <paul@buetow.org>2026-03-20 21:38:40 +0200
commit8701f048229e0bbb2d97ba23a9ba38397eb70286 (patch)
tree4e76c410d8d8620ee0353fd1960f3c0ace76ac5d
parentf444b25bc2f7f92ab0bbd5411e7dd89f49fa25d2 (diff)
internal/rpn/cmd/perc: add calc/rpn subcommands and fix assignment handling
- Add calc and rpn subcommands to main.go - Support RPN expression evaluation via perc calc/rpn <expr> - Support variable assignment format: 'name value = expr...' - Fix assignment parsing to handle 'name value = expression' format - All tests pass, go vet passes
-rw-r--r--RPN_IMPLEMENTATION.md158
-rw-r--r--cmd/perc/main.go31
-rw-r--r--internal/rpn/rpn.go63
3 files changed, 234 insertions, 18 deletions
diff --git a/RPN_IMPLEMENTATION.md b/RPN_IMPLEMENTATION.md
new file mode 100644
index 0000000..0b4a7cb
--- /dev/null
+++ b/RPN_IMPLEMENTATION.md
@@ -0,0 +1,158 @@
+# RPN (Postfix Notation) Stack Calculator Implementation Plan
+
+## Context
+
+The `perc` project is a percentage calculator that currently supports three formats:
+- `20% of 150`
+- `30 is what % of 150`
+- `30 is 20% of what`
+
+Users want to extend it to support postfix notation (Reverse Polish Notation) stack-based calculations like `3 4 + 4 4 - *`, with variable assignments and reuse capabilities.
+
+## Requirements
+
+### Core Features
+- **Postfix Notation Parser**: Parse space-separated tokens where numbers are pushed to a stack and operators pop operands and push results
+- **Arithmetic Operations**: Addition (+), subtraction (-), multiplication (*), division (/), power (^), modulo (%)
+- **Variable Support**:
+ - Assign: `varname value =` stores value in variable
+ - Reuse: `varname` pushes stored value onto stack
+ - Delete: `varname d` removes variable
+ - List: `vars` shows all variables
+ - Clear: `clear` removes all variables
+- **Stack Inspection**: `dup` (duplicate top), `swap` (swap top two), `pop` (remove top), `show` (print stack)
+- **Error Handling**: Division by zero, invalid operators, insufficient operands, undefined variables
+- **Input Methods**: Support both `perc calc 3 4 +` and `perc rpn 3 4 +` syntax
+
+## Task Structure
+
+### Core Implementation (Tasks 401-403)
+| Task ID | Description | Dependencies |
+|---------|-------------|--------------|
+| 401 | Create `internal/rpn/variables.go` - Variable storage and management | None |
+| 402 | Create `internal/rpn/operations.go` - Operator implementations and stack manipulation | 401 |
+| 403 | Create `internal/rpn/rpn.go` - RPN parser and evaluator | 401, 402 |
+
+### Integration (Tasks 404-408)
+| Task ID | Description | Dependencies |
+|---------|-------------|--------------|
+| 404 | Add `ParseRPN()` to `internal/calculator/calculator.go` | 403 |
+| 405 | Update `cmd/perc/main.go` - Add calc/rpn subcommands | 404 |
+| 406 | Update `internal/repl/repl.go` - Handle RPN input | 405 |
+| 407 | Update `internal/repl/commands.go` - Add rpn command | 406 |
+| 408 | Update `Magefile.go` - Add RPN() target | 407 |
+
+## Implementation Approach
+
+### File Structure
+```
+internal/
+├── calculator/
+│ ├── calculator.go # Existing - add RPN support
+│ └── calculator_test.go # Add RPN tests
+├── rpn/
+│ ├── rpn.go # New: RPN parser and evaluator
+│ ├── operations.go # New: Operator implementations
+│ └── variables.go # New: Variable storage and management
+└── repl/
+ ├── repl.go # Update to support RPN
+ └── commands.go # Add rpn command
+
+cmd/perc/
+└── main.go # Add calc/rpn subcommand support
+```
+
+### Key Components
+
+**`internal/rpn/rpn.go`**:
+- `ParseAndEvaluate(input string) (string, error)` - Main entry point
+- Tokenize input into numbers, operators, variables
+- Execute RPN evaluation using stack
+
+**`internal/rpn/operations.go`**:
+- Operator functions: add, subtract, multiply, divide, power, modulo
+- Stack manipulation: dup, swap, pop, show
+- Error handling for invalid operations
+
+**`internal/rpn/variables.go`**:
+- Variable storage map
+- `SetVariable(name string, value float64)` - Assign: `name value =`
+- `GetVariable(name string)` - Retrieve variable value
+- `DeleteVariable(name string)` - Delete: `name d`
+- `ListVariables()` - List all: `vars`
+- `ClearVariables()` - Clear all: `clear`
+
+**`internal/calculator/calculator.go`**:
+- Add `ParseRPN(input string) (string, error)` function
+- Integrate with existing `Parse()` to detect RPN format
+
+**`cmd/perc/main.go`**:
+- Add `calc` and `rpn` subcommand support
+- Route to appropriate parser based on command
+
+**`internal/repl/repl.go`**:
+- Update executor to handle RPN input
+- Update commands.go to include `rpn` as built-in command
+
+**`Magefile.go`**:
+- Add `RPN()` target for testing
+
+## Usage Examples
+
+```bash
+# Basic arithmetic
+perc calc 3 4 + # → 7
+perc calc 3 4 + 4 4 - * # → 0
+
+# Power and modulo
+perc calc 2 3 ^ # → 8
+perc calc 10 3 % # → 1
+
+# Variables
+perc calc x 5 = x x + # → 10
+perc calc pi 3.14159 = pi 2 * # → 6.28318
+
+# Stack operations
+perc calc 1 2 3 dup # → 1 2 3 3
+perc calc 1 2 swap # → 2 1
+perc calc 1 2 3 pop # → 1 2
+
+# Variable management
+perc calc vars # List all variables
+perc calc x d # Delete variable x
+perc calc clear # Clear all variables
+```
+
+## Testing Strategy
+
+### Unit Tests
+- Each operation function (add, sub, mul, div, pow, mod)
+- Variable operations (set, get, delete, list, clear)
+- RPN tokenization and evaluation
+
+### Integration Tests
+- Full RPN evaluation with mixed operations
+- Variable assignment and reuse
+- Error cases (division by zero, undefined variables, insufficient operands)
+
+### Manual Testing
+1. `perc calc 3 4 +` - should output `7`
+2. `perc calc 3 4 + 4 4 - *` - should output `0`
+3. `perc calc 2 3 ^` - should output `8`
+4. `perc calc x 5 = x x +` - should output `10`
+5. `perc calc vars` - should list variables
+6. `perc calc clear` - should clear all variables
+7. `mage rpn` - verify Mage integration
+
+## Task UUIDs for Reference
+
+| Task ID | UUID |
+|---------|------|
+| 401 | (see `task 401 _uuid`) |
+| 402 | (see `task 402 _uuid`) |
+| 403 | (see `task 403 _uuid`) |
+| 404 | (see `task 404 _uuid`) |
+| 405 | (see `task 405 _uuid`) |
+| 406 | (see `task 406 _uuid`) |
+| 407 | (see `task 407 _uuid`) |
+| 408 | (see `task 408 _uuid`) |
diff --git a/cmd/perc/main.go b/cmd/perc/main.go
index a9faf76..d33593c 100644
--- a/cmd/perc/main.go
+++ b/cmd/perc/main.go
@@ -7,6 +7,7 @@ import (
"codeberg.org/snonux/perc/internal"
"codeberg.org/snonux/perc/internal/calculator"
+ "codeberg.org/snonux/perc/internal/rpn"
"codeberg.org/snonux/perc/internal/repl"
"github.com/mattn/go-isatty"
)
@@ -41,6 +42,19 @@ func runCommand(args []string) (string, error) {
return "", nil
}
+ // Check for calc subcommand
+ if args[1] == "calc" || args[1] == "rpn" {
+ if len(args) < 3 {
+ return "", fmt.Errorf("missing expression after '%s'", args[1])
+ }
+ input := strings.Join(args[2:], " ")
+ result, err := runRPN(input)
+ if err != nil {
+ return "", err
+ }
+ return result, nil
+ }
+
input := strings.Join(args[1:], " ")
result, err := calculator.Parse(input)
if err != nil {
@@ -50,15 +64,30 @@ func runCommand(args []string) (string, error) {
return result, nil
}
+// runRPN parses and evaluates an RPN expression
+func runRPN(input string) (string, error) {
+ vars := rpn.NewVariables().(*rpn.Variables)
+ rpnCalc := rpn.NewRPN(vars)
+ return rpnCalc.ParseAndEvaluate(input)
+}
+
func printUsage() {
fmt.Println("Usage: perc <calculation>")
+ fmt.Println(" perc calc <rpn-expression>")
+ fmt.Println(" perc rpn <rpn-expression>")
fmt.Println(" perc version")
fmt.Println(" perc [--repl|repl]")
- fmt.Println("\nExamples:")
+ fmt.Println("\nPercentage calculator examples:")
fmt.Println(" perc 20% of 150")
fmt.Println(" perc what is 20% of 150")
fmt.Println(" perc 30 is what % of 150")
fmt.Println(" perc 30 is 20% of what")
+ fmt.Println("\nRPN (postfix notation) examples:")
+ fmt.Println(" perc calc 3 4 +")
+ fmt.Println(" perc calc 3 4 + 4 4 - *")
+ fmt.Println(" perc calc x = 5 x x +")
+ fmt.Println(" perc calc 2 3 ^")
+ fmt.Println(" perc calc dup swap pop show")
fmt.Println("\nStart REPL mode interactively by running without arguments:")
fmt.Println(" perc")
}
diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go
index f41df1c..f99d1a6 100644
--- a/internal/rpn/rpn.go
+++ b/internal/rpn/rpn.go
@@ -30,8 +30,8 @@ func (r *RPN) ParseAndEvaluate(input string) (string, error) {
return "", fmt.Errorf("empty expression")
}
- // First check for special assignment pattern: "name value ="
- // This is handled separately from RPN evaluation
+ // Handle single assignment: "name value ="
+ // This is when the entire input is just an assignment
if strings.Contains(input, " = ") {
parts := strings.SplitN(input, " = ", 2)
if len(parts) == 2 {
@@ -40,16 +40,51 @@ func (r *RPN) ParseAndEvaluate(input string) (string, error) {
// Validate name is a single word (variable name)
nameFields := strings.Fields(name)
if len(nameFields) == 1 {
- // Parse the value
- val, err := strconv.ParseFloat(valueStr, 64)
- if err != nil {
- return "", fmt.Errorf("invalid value '%s' for assignment: %w", valueStr, err)
+ // Validate value is a single number
+ valueFields := strings.Fields(valueStr)
+ if len(valueFields) == 1 {
+ val, err := strconv.ParseFloat(valueFields[0], 64)
+ if err != nil {
+ return "", fmt.Errorf("invalid value '%s' for assignment: %w", valueFields[0], err)
+ }
+ if err := r.vars.SetVariable(nameFields[0], val); err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("%s = %.10g", nameFields[0], val), nil
}
- // Assign the variable
- if err := r.vars.SetVariable(nameFields[0], val); err != nil {
- return "", err
+ }
+ }
+ }
+
+ // Handle assignment with expression: "name value = expression..."
+ // Format: variable_name value = expression (where = comes after value)
+ if strings.Contains(input, " = ") {
+ // Check if the input matches pattern: "name value = expr..."
+ // where name and value are single tokens, and = comes after value
+ // For example: "x 5 = x x +" or "pi 3.14 = pi 2 *"
+
+ // Find " = " position and split
+ pos := strings.Index(input, " = ")
+ if pos >= 0 {
+ before := input[:pos] // "name value"
+ after := input[pos+3:] // "expr..."
+
+ beforeFields := strings.Fields(before)
+ if len(beforeFields) == 2 {
+ name := beforeFields[0]
+ valueStr := beforeFields[1]
+
+ // Try to parse value as a number
+ val, err := strconv.ParseFloat(valueStr, 64)
+ if err == nil {
+ // Valid assignment pattern: "name value = expr..."
+ if err := r.vars.SetVariable(name, val); err != nil {
+ return "", err
+ }
+
+ // Evaluate the remaining expression
+ return r.evaluate(strings.Fields(strings.TrimSpace(after)))
}
- return fmt.Sprintf("%s = %.10g", nameFields[0], val), nil
}
}
}
@@ -75,12 +110,7 @@ func (r *RPN) evaluate(tokens []string) (string, error) {
for i, token := range tokens {
// Check for variable assignment: name value =
if token == "=" {
- // Assignment requires: variable_name (as previous token), value (on stack)
- // But tokens are processed linearly, so we need special handling
- // For "name value =", we have tokens: [name, value, =]
- // When we see =, we need to have the value on stack and name from before
- // This approach won't work well, so we handle assignment at parse time
- return "", fmt.Errorf("invalid assignment: '=' must be used with 'name value =' syntax")
+ return "", fmt.Errorf("invalid assignment syntax at token %d: 'name value =' requires spaces around =", i)
}
// Check if it's a number
@@ -135,7 +165,6 @@ func (r *RPN) evaluate(tokens []string) (string, error) {
if err != nil {
return "", fmt.Errorf("show: %w", err)
}
- // For show, we return the stack state instead of continuing
return result, nil
case "vars":
result, err := r.ops.ListVariables()