From f444b25bc2f7f92ab0bbd5411e7dd89f49fa25d2 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 20 Mar 2026 21:36:56 +0200 Subject: 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 --- internal/rpn/rpn_test.go | 420 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 internal/rpn/rpn_test.go (limited to 'internal/rpn/rpn_test.go') 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) + } +} -- cgit v1.2.3