summaryrefslogtreecommitdiff
path: root/internal/rpn/rpn_test.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-20 21:36:56 +0200
committerPaul Buetow <paul@buetow.org>2026-03-20 21:36:56 +0200
commitf444b25bc2f7f92ab0bbd5411e7dd89f49fa25d2 (patch)
tree0bb7a6439b456f0695641ce0cc9c2eb8739a2c73 /internal/rpn/rpn_test.go
parentaf8d680f3a1271fe6f57c5bba7675e5f06e0d2fd (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/rpn/rpn_test.go')
-rw-r--r--internal/rpn/rpn_test.go420
1 files changed, 420 insertions, 0 deletions
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)
+ }
+}