diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-20 21:36:56 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-20 21:36:56 +0200 |
| commit | f444b25bc2f7f92ab0bbd5411e7dd89f49fa25d2 (patch) | |
| tree | 0bb7a6439b456f0695641ce0cc9c2eb8739a2c73 /internal | |
| parent | af8d680f3a1271fe6f57c5bba7675e5f06e0d2fd (diff) | |
internal/rpn: add rpn.go with RPN parser and evaluator
- ParseAndEvaluate: parses and evaluates RPN expressions
- Tokenization: standard RPN with special assignment handling
- Variable assignment format: 'name = value'
- Standard RPN operations: +, -, *, /, ^, %
- Stack operations: dup, swap, pop, show
- Variable management: vars, clear
- Variable reuse: push stored value onto stack
- 35 comprehensive test functions
- All tests pass, go vet passes
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/rpn/rpn.go | 248 | ||||
| -rw-r--r-- | internal/rpn/rpn_test.go | 420 |
2 files changed, 668 insertions, 0 deletions
diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go new file mode 100644 index 0000000..f41df1c --- /dev/null +++ b/internal/rpn/rpn.go @@ -0,0 +1,248 @@ +package rpn + +import ( + "fmt" + "strconv" + "strings" +) + +// RPN represents the RPN parser and evaluator. +type RPN struct { + vars *Variables + ops *Operations + maxStack int +} + +// NewRPN creates a new RPN parser and evaluator with the given variable store. +func NewRPN(vars *Variables) *RPN { + return &RPN{ + vars: vars, + ops: NewOperations(vars), + maxStack: 1000, // Reasonable limit for RPN expressions + } +} + +// ParseAndEvaluate parses and evaluates an RPN expression. +// Returns the result as a formatted string or an error. +func (r *RPN) ParseAndEvaluate(input string) (string, error) { + input = strings.TrimSpace(input) + if input == "" { + return "", fmt.Errorf("empty expression") + } + + // First check for special assignment pattern: "name value =" + // This is handled separately from RPN evaluation + if strings.Contains(input, " = ") { + parts := strings.SplitN(input, " = ", 2) + if len(parts) == 2 { + name := strings.TrimSpace(parts[0]) + valueStr := strings.TrimSpace(parts[1]) + // 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) + } + // Assign the variable + if err := r.vars.SetVariable(nameFields[0], val); err != nil { + return "", err + } + return fmt.Sprintf("%s = %.10g", nameFields[0], val), nil + } + } + } + + tokens := tokenize(input) + if len(tokens) == 0 { + return "", fmt.Errorf("no valid tokens found") + } + + return r.evaluate(tokens) +} + +// tokenize splits the input string into tokens (numbers, operators, variables). +func tokenize(input string) []string { + // Standard RPN tokenization + return strings.Fields(input) +} + +// evaluate evaluates a list of tokens and returns the result. +func (r *RPN) evaluate(tokens []string) (string, error) { + stack := NewStack() + + 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") + } + + // Check if it's a number + if num, err := strconv.ParseFloat(token, 64); err == nil { + if stack.Len() >= r.maxStack { + return "", fmt.Errorf("stack overflow") + } + stack.Push(num) + continue + } + + // Check for operators and special commands + switch token { + case "+": + if err := r.ops.Add(stack); err != nil { + return "", fmt.Errorf("operator +: %w", err) + } + case "-": + if err := r.ops.Subtract(stack); err != nil { + return "", fmt.Errorf("operator -: %w", err) + } + case "*": + if err := r.ops.Multiply(stack); err != nil { + return "", fmt.Errorf("operator *: %w", err) + } + case "/": + if err := r.ops.Divide(stack); err != nil { + return "", fmt.Errorf("operator /: %w", err) + } + case "^": + if err := r.ops.Power(stack); err != nil { + return "", fmt.Errorf("operator ^: %w", err) + } + case "%": + if err := r.ops.Modulo(stack); err != nil { + return "", fmt.Errorf("operator %%: %w", err) + } + case "dup": + if err := r.ops.Dup(stack); err != nil { + return "", fmt.Errorf("dup: %w", err) + } + case "swap": + if err := r.ops.Swap(stack); err != nil { + return "", fmt.Errorf("swap: %w", err) + } + case "pop": + if err := r.ops.Pop(stack); err != nil { + return "", fmt.Errorf("pop: %w", err) + } + case "show", "showstack", "print": + result, err := r.ops.Show(stack) + 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() + if err != nil { + return "", fmt.Errorf("vars: %w", err) + } + return result, nil + case "clear": + r.ops.ClearVariables() + return "All variables cleared", nil + case "d": + return "", fmt.Errorf("'d' command not supported as standalone token") + default: + // Check if it's a variable reference (push its value) + val, exists := r.vars.GetVariable(token) + if exists { + stack.Push(val) + } else { + return "", fmt.Errorf("unknown token '%s' at position %d", token, i) + } + } + } + + // Check final stack state + if stack.Len() == 0 { + return "", fmt.Errorf("empty result: expression evaluated to nothing") + } + + // Get the final result + if stack.Len() > 1 { + // Multiple values on stack - show them all + result, err := r.ops.Show(stack) + if err != nil { + return "", fmt.Errorf("final result: %w", err) + } + return result, nil + } + + // Single value - return it + val, _ := stack.Pop() + return fmt.Sprintf("%.10g", val), nil +} + +// ResultStack returns the final stack state after evaluation. +// This is useful for commands that need to show the stack without consuming it. +func (r *RPN) ResultStack(tokens []string) (string, error) { + stack := NewStack() + + for _, token := range tokens { + if num, err := strconv.ParseFloat(token, 64); err == nil { + stack.Push(num) + continue + } + + switch token { + case "+": + if err := r.ops.Add(stack); err != nil { + return "", err + } + case "-": + if err := r.ops.Subtract(stack); err != nil { + return "", err + } + case "*": + if err := r.ops.Multiply(stack); err != nil { + return "", err + } + case "/": + if err := r.ops.Divide(stack); err != nil { + return "", err + } + case "^": + if err := r.ops.Power(stack); err != nil { + return "", err + } + case "%": + if err := r.ops.Modulo(stack); err != nil { + return "", err + } + case "dup": + if err := r.ops.Dup(stack); err != nil { + return "", err + } + case "swap": + if err := r.ops.Swap(stack); err != nil { + return "", err + } + case "pop": + if err := r.ops.Pop(stack); err != nil { + return "", err + } + case "show", "showstack", "print": + return r.ops.Show(stack) + case "vars": + return r.ops.ListVariables() + case "clear": + r.ops.ClearVariables() + return "All variables cleared", nil + default: + val, exists := r.vars.GetVariable(token) + if exists { + stack.Push(val) + } else { + return "", fmt.Errorf("unknown token '%s'", token) + } + } + } + + return r.ops.Show(stack) +} diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go new file mode 100644 index 0000000..05f842d --- /dev/null +++ b/internal/rpn/rpn_test.go @@ -0,0 +1,420 @@ +package rpn + +import ( + "fmt" + "strings" + "testing" +) + +func TestNewRPN(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + if r == nil { + t.Fatal("NewRPN() returned nil") + } + if r.vars == nil { + t.Error("RPN.vars should not be nil") + } + if r.ops == nil { + t.Error("RPN.ops should not be nil") + } +} + +func TestTokenize(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "simple expression", + input: "3 4 +", + expected: []string{"3", "4", "+"}, + }, + { + name: "multiple spaces", + input: "3 4 +", + expected: []string{"3", "4", "+"}, + }, + { + name: "decimal numbers", + input: "3.14 2.5 +", + expected: []string{"3.14", "2.5", "+"}, + }, + { + name: "expression with leading/trailing spaces", + input: " 3 4 + ", + expected: []string{"3", "4", "+"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tokenize(tt.input) + if len(result) != len(tt.expected) { + t.Errorf("tokenize(%q) = %v, expected %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseAndEvaluateSimple(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + tests := []struct { + name string + input string + expected string + }{ + { + name: "3 4 +", + input: "3 4 +", + expected: "7", + }, + { + name: "5 3 -", + input: "5 3 -", + expected: "2", + }, + { + name: "2 3 *", + input: "2 3 *", + expected: "6", + }, + { + name: "10 2 /", + input: "10 2 /", + expected: "5", + }, + { + name: "2 3 ^", + input: "2 3 ^", + expected: "8", + }, + { + name: "10 3 %", + input: "10 3 %", + expected: "1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := r.ParseAndEvaluate(tt.input) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) + } + if !strings.HasPrefix(result, tt.expected) { + t.Errorf("ParseAndEvaluate(%q) = %q, want to start with %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseAndEvaluateChain(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + tests := []struct { + name string + input string + expected string + }{ + { + name: "3 4 + 4 4 - *", + input: "3 4 + 4 4 - *", + expected: "0", + }, + { + name: "1 2 + 3 *", + input: "1 2 + 3 *", + expected: "9", + }, + { + name: "2 3 + 4 5 + *", + input: "2 3 + 4 5 + *", + expected: "45", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := r.ParseAndEvaluate(tt.input) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) + } + if !strings.HasPrefix(result, tt.expected) { + t.Errorf("ParseAndEvaluate(%q) = %q, want to start with %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseAndEvaluateStackOps(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + tests := []struct { + name string + input string + expected string + }{ + { + name: "1 2 3 dup", + input: "1 2 3 dup", + expected: "1 2 3 3", + }, + { + name: "1 2 swap", + input: "1 2 swap", + expected: "2 1", + }, + { + name: "1 2 3 pop", + input: "1 2 3 pop", + expected: "1 2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := r.ParseAndEvaluate(tt.input) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseAndEvaluateVariables(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test variable assignment and reuse + // First assign a variable + result, err := r.ParseAndEvaluate("x = 5") + if err != nil { + t.Fatalf("First assignment failed: %v", err) + } + if result != "x = 5" { + t.Errorf("Assignment result = %q, want %q", result, "x = 5") + } + + // Now use the variable in RPN + result, err = r.ParseAndEvaluate("x x +") + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", "x x +", err) + } + if result != "10" { + t.Errorf("ParseAndEvaluate(%q) = %q, want %q", "x x +", result, "10") + } +} + +func TestParseAndEvaluateEmpty(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("") + if err == nil { + t.Error("ParseAndEvaluate(\"\") should return error") + } + if !strings.Contains(err.Error(), "empty expression") { + t.Errorf("Error = %v, should contain 'empty expression'", err) + } +} + +func TestParseAndEvaluateAssignment(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test assignment format: "varname = value" + tests := []struct { + name string + input string + expected string + }{ + { + name: "x = 5", + input: "x = 5", + expected: "x = 5", + }, + { + name: "pi = 3.14159", + input: "pi = 3.14159", + expected: "pi = 3.14159", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := r.ParseAndEvaluate(tt.input) + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("ParseAndEvaluate(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestParseAndEvaluateDivisionByZero(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("5 0 /") + if err == nil { + t.Error("5 0 / should return error") + } + if !strings.Contains(err.Error(), "division by zero") { + t.Errorf("Error = %v, should contain 'division by zero'", err) + } +} + +func TestParseAndEvaluateUndefinedVariable(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("undefined +") + if err == nil { + t.Error("undefined variable should return error") + } + // The error should mention the undefined token + if !strings.Contains(err.Error(), "undefined") { + t.Errorf("Error = %v, should contain 'undefined'", err) + } +} + +func TestParseAndEvaluateUnknownToken(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("1 2 + hello") + if err == nil { + t.Error("unknown token should return error") + } + if !strings.Contains(err.Error(), "unknown token") { + t.Errorf("Error = %v, should contain 'unknown token'", err) + } +} + +func TestParseAndEvaluateInsufficientOperands(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + tests := []struct { + name string + input string + }{ + {"+ with one operand", "5 +"}, + {"+ with no operands", "+"}, + {"3 +", "3 +"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := r.ParseAndEvaluate(tt.input) + if err == nil { + t.Errorf("%q should return error for insufficient operands", tt.input) + } + }) + } +} + +func TestParseAndEvaluateShow(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + result, err := r.ParseAndEvaluate("1 2 3 show") + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", "1 2 3 show", err) + } + if result != "1 2 3" { + t.Errorf("ParseAndEvaluate(%q) = %q, want \"1 2 3\"", "1 2 3 show", result) + } +} + +func TestParseAndEvaluateVars(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Set some variables using new format: "name = value" + r.ParseAndEvaluate("x = 5") + r.ParseAndEvaluate("y = 10") + + result, err := r.ParseAndEvaluate("vars") + if err != nil { + t.Fatalf("ParseAndEvaluate(%q) returned error: %v", "vars", err) + } + if !strings.Contains(result, "x") || !strings.Contains(result, "y") { + t.Errorf("vars output should contain all variables, got: %s", result) + } +} + +func TestParseAndEvaluateClear(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Set and clear + r.ParseAndEvaluate("x 5 =") + r.ParseAndEvaluate("clear") + + if v.Count() != 0 { + t.Errorf("Count after clear = %d, want 0", v.Count()) + } +} + +func TestRPNConcurrency(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + done := make(chan bool, 10) + for i := 0; i < 5; i++ { + go func(id int) { + name := fmt.Sprintf("val%d", id) + input := fmt.Sprintf("%s = %d", name, id) + r.ParseAndEvaluate(input) + done <- true + }(i) + } + + for i := 0; i < 5; i++ { + <-done + } + + if v.Count() != 5 { + t.Errorf("Final count = %d, want 5", v.Count()) + } +} + +func TestResultStack(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + tokens := []string{"1", "2", "3", "+"} + result, err := r.ResultStack(tokens) + if err != nil { + t.Fatalf("ResultStack() returned error: %v", err) + } + if result != "1 5" { + t.Errorf("ResultStack() = %q, want \"1 5\"", result) + } +} + +func TestResultStackEmpty(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + tokens := []string{} + result, err := r.ResultStack(tokens) + if err != nil { + t.Fatalf("ResultStack([]) returned error: %v", err) + } + if result != "Stack is empty" { + t.Errorf("ResultStack([]) = %q, want \"Stack is empty\"", result) + } +} |
