From 8701f048229e0bbb2d97ba23a9ba38397eb70286 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 20 Mar 2026 21:38:40 +0200 Subject: 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 - Support variable assignment format: 'name value = expr...' - Fix assignment parsing to handle 'name value = expression' format - All tests pass, go vet passes --- RPN_IMPLEMENTATION.md | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/perc/main.go | 31 +++++++++- internal/rpn/rpn.go | 63 ++++++++++++++------ 3 files changed, 234 insertions(+), 18 deletions(-) create mode 100644 RPN_IMPLEMENTATION.md 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 ") + fmt.Println(" perc calc ") + fmt.Println(" perc rpn ") 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() -- cgit v1.2.3