diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-20 21:34:40 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-20 21:34:40 +0200 |
| commit | af8d680f3a1271fe6f57c5bba7675e5f06e0d2fd (patch) | |
| tree | 60e0eea374479ccb55b3cb3b28c806a55b1d29a0 /internal | |
| parent | ae780ab2d6234e62fc716099405fb9941d86c0c3 (diff) | |
internal/rpn: add operations.go with operator implementations
- Arithmetic: +, -, *, /, ^, %
- Stack manipulation: dup, swap, pop, show
- Variable operations: assign, use, delete, list, clear
- Thread-safe with sync.RWMutex in Variables
- Comprehensive test coverage: 37 test functions
- All tests pass, go vet passes
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/rpn/operations.go | 247 | ||||
| -rw-r--r-- | internal/rpn/operations_test.go | 548 | ||||
| -rw-r--r-- | internal/rpn/variables.go | 55 |
3 files changed, 850 insertions, 0 deletions
diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go new file mode 100644 index 0000000..1248369 --- /dev/null +++ b/internal/rpn/operations.go @@ -0,0 +1,247 @@ +package rpn + +import ( + "fmt" + "math" +) + +// Operations provides operator implementations and stack manipulation. +type Operations struct { + vars *Variables +} + +// NewOperations creates a new Operations instance with the given variable store. +func NewOperations(vars *Variables) *Operations { + return &Operations{ + vars: vars, + } +} + +// arithmetic operators + +// Add pops two values from stack, adds them, and pushes result. +func (o *Operations) Add(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for +: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for +: %w", err) + } + + stack.Push(a + b) + return nil +} + +// Subtract pops two values from stack, subtracts (a - b), and pushes result. +func (o *Operations) Subtract(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for -: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for -: %w", err) + } + + stack.Push(a - b) + return nil +} + +// Multiply pops two values from stack, multiplies them, and pushes result. +func (o *Operations) Multiply(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for *: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for *: %w", err) + } + + stack.Push(a * b) + return nil +} + +// Divide pops two values from stack, divides (a / b), and pushes result. +func (o *Operations) Divide(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for /: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for /: %w", err) + } + + if b == 0 { + return fmt.Errorf("division by zero") + } + + stack.Push(a / b) + return nil +} + +// Power pops two values from stack, raises first to power of second (a ^ b), and pushes result. +func (o *Operations) Power(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for ^: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for ^: %w", err) + } + + stack.Push(math.Pow(a, b)) + return nil +} + +// Modulo pops two values from stack, computes modulo (a % b), and pushes result. +func (o *Operations) Modulo(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for %%: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for %%: %w", err) + } + + if b == 0 { + return fmt.Errorf("modulo by zero") + } + + stack.Push(math.Mod(a, b)) + return nil +} + +// stack manipulation operators + +// Dup duplicates the top stack value. +func (o *Operations) Dup(stack *Stack) error { + val, err := stack.Peek() + if err != nil { + return fmt.Errorf("insufficient operands for dup: %w", err) + } + stack.Push(val) + return nil +} + +// Swap swaps the top two stack values. +func (o *Operations) Swap(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for swap: need at least 2 values") + } + + // Get the values without popping + vals := stack.Values() + top := vals[len(vals)-1] + second := vals[len(vals)-2] + + // Pop both + stack.Pop() + stack.Pop() + + // Push in swapped order + stack.Push(top) + stack.Push(second) + + return nil +} + +// Pop removes and discards the top stack value. +func (o *Operations) Pop(stack *Stack) error { + _, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for pop: %w", err) + } + return nil +} + +// Show returns the current stack as a formatted string. +func (o *Operations) Show(stack *Stack) (string, error) { + if stack.Len() == 0 { + return "Stack is empty", nil + } + + vals := stack.Values() + var result string + for i, val := range vals { + if i > 0 { + result += " " + } + result += fmt.Sprintf("%.10g", val) + } + return result, nil +} + +// variables operations + +// AssignVariable assigns a value from stack to a variable. +// Usage: `name value =` +func (o *Operations) AssignVariable(stack *Stack, name string) error { + if name == "" { + return fmt.Errorf("variable name cannot be empty") + } + + if stack.Len() < 1 { + return fmt.Errorf("insufficient operands for assignment: need value") + } + + val, err := stack.Pop() + if err != nil { + return err + } + + return o.vars.SetVariable(name, val) +} + +// UseVariable pushes a variable's value onto the stack. +// Usage: `varname` (pushes stored value) +func (o *Operations) UseVariable(stack *Stack, name string) error { + if name == "" { + return fmt.Errorf("variable name cannot be empty") + } + + val, exists := o.vars.GetVariable(name) + if !exists { + return fmt.Errorf("undefined variable: %s", name) + } + + stack.Push(val) + return nil +} + +// DeleteVariable removes a variable. +// Usage: `name d` +func (o *Operations) DeleteVariable(name string) error { + if name == "" { + return fmt.Errorf("variable name cannot be empty") + } + + deleted := o.vars.DeleteVariable(name) + if !deleted { + return fmt.Errorf("undefined variable: %s", name) + } + return nil +} + +// ListVariables lists all variables. +// Usage: `vars` +func (o *Operations) ListVariables() (string, error) { + return o.vars.FormatVariables(), nil +} + +// ClearVariables removes all variables. +// Usage: `clear` +func (o *Operations) ClearVariables() { + o.vars.ClearVariables() +} diff --git a/internal/rpn/operations_test.go b/internal/rpn/operations_test.go new file mode 100644 index 0000000..0083196 --- /dev/null +++ b/internal/rpn/operations_test.go @@ -0,0 +1,548 @@ +package rpn + +import ( + "fmt" + "strings" + "testing" +) + +func TestStackNewStack(t *testing.T) { + s := NewStack() + if s == nil { + t.Fatal("NewStack() returned nil") + } + if s.Len() != 0 { + t.Errorf("NewStack() length = %d, want 0", s.Len()) + } +} + +func TestStackPushPop(t *testing.T) { + s := NewStack() + s.Push(1.0) + s.Push(2.0) + s.Push(3.0) + + if s.Len() != 3 { + t.Errorf("Length after 3 pushes = %d, want 3", s.Len()) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() returned error: %v", err) + } + if val != 3.0 { + t.Errorf("Pop() = %v, want 3.0", val) + } + + if s.Len() != 2 { + t.Errorf("Length after pop = %d, want 2", s.Len()) + } +} + +func TestStackPeek(t *testing.T) { + s := NewStack() + s.Push(5.0) + + val, err := s.Peek() + if err != nil { + t.Fatalf("Peek() returned error: %v", err) + } + if val != 5.0 { + t.Errorf("Peek() = %v, want 5.0", val) + } + + // Peek should not remove the value + if s.Len() != 1 { + t.Errorf("Length after Peek() = %d, want 1", s.Len()) + } +} + +func TestStackPeekEmpty(t *testing.T) { + s := NewStack() + _, err := s.Peek() + if err == nil { + t.Error("Peek() on empty stack should return error") + } + if !strings.Contains(err.Error(), "stack is empty") { + t.Errorf("Peek() error = %v, should contain 'stack is empty'", err) + } +} + +func TestStackPopEmpty(t *testing.T) { + s := NewStack() + _, err := s.Pop() + if err == nil { + t.Error("Pop() on empty stack should return error") + } +} + +func TestStackValues(t *testing.T) { + s := NewStack() + s.Push(1.0) + s.Push(2.0) + s.Push(3.0) + + vals := s.Values() + if len(vals) != 3 { + t.Errorf("Values() length = %d, want 3", len(vals)) + } + + // Values() returns values in storage order (bottom-to-top) + // Push order: 1, 2, 3 so storage is [1, 2, 3] with 3 on top + if vals[0] != 1.0 || vals[1] != 2.0 || vals[2] != 3.0 { + t.Errorf("Values() = %v, want [1 2 3] (bottom-to-top)", vals) + } +} + +func TestStackClear(t *testing.T) { + s := NewStack() + s.Push(1.0) + s.Push(2.0) + s.Push(3.0) + + s.Clear() + + if s.Len() != 0 { + t.Errorf("Length after Clear() = %d, want 0", s.Len()) + } +} + +func TestOperationsAdd(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(3.0) + s.Push(4.0) + + err := o.Add(s) + if err != nil { + t.Fatalf("Add() returned error: %v", err) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() after Add() returned error: %v", err) + } + if val != 7.0 { + t.Errorf("Add result = %v, want 7.0", val) + } +} + +func TestOperationsSubtract(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(10.0) + s.Push(4.0) + + err := o.Subtract(s) + if err != nil { + t.Fatalf("Subtract() returned error: %v", err) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() after Subtract() returned error: %v", err) + } + if val != 6.0 { + t.Errorf("Subtract result = %v, want 6.0 (10 - 4)", val) + } +} + +func TestOperationsMultiply(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(5.0) + s.Push(3.0) + + err := o.Multiply(s) + if err != nil { + t.Fatalf("Multiply() returned error: %v", err) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() after Multiply() returned error: %v", err) + } + if val != 15.0 { + t.Errorf("Multiply result = %v, want 15.0", val) + } +} + +func TestOperationsDivide(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(20.0) + s.Push(4.0) + + err := o.Divide(s) + if err != nil { + t.Fatalf("Divide() returned error: %v", err) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() after Divide() returned error: %v", err) + } + if val != 5.0 { + t.Errorf("Divide result = %v, want 5.0", val) + } +} + +func TestOperationsDivideByZero(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(10.0) + s.Push(0.0) + + err := o.Divide(s) + if err == nil { + t.Error("Divide by zero should return error") + } + if !strings.Contains(err.Error(), "division by zero") { + t.Errorf("Divide by zero error = %v, should contain 'division by zero'", err) + } +} + +func TestOperationsPower(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(2.0) + s.Push(3.0) + + err := o.Power(s) + if err != nil { + t.Fatalf("Power() returned error: %v", err) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() after Power() returned error: %v", err) + } + if val != 8.0 { + t.Errorf("Power result = %v, want 8.0 (2^3)", val) + } +} + +func TestOperationsModulo(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(10.0) + s.Push(3.0) + + err := o.Modulo(s) + if err != nil { + t.Fatalf("Modulo() returned error: %v", err) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() after Modulo() returned error: %v", err) + } + if val != 1.0 { + t.Errorf("Modulo result = %v, want 1.0 (10 %% 3)", val) + } +} + +func TestOperationsModuloByZero(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(10.0) + s.Push(0.0) + + err := o.Modulo(s) + if err == nil { + t.Error("Modulo by zero should return error") + } +} + +func TestOperationsInsufficientOperands(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(5.0) + + // Try to add with only one operand + err := o.Add(s) + if err == nil { + t.Error("Add with insufficient operands should return error") + } +} + +func TestOperationsDup(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(7.0) + + err := o.Dup(s) + if err != nil { + t.Fatalf("Dup() returned error: %v", err) + } + + if s.Len() != 2 { + t.Errorf("Length after Dup() = %d, want 2", s.Len()) + } + + val1, _ := s.Pop() + val2, _ := s.Pop() + if val1 != 7.0 || val2 != 7.0 { + t.Errorf("Dup values = %v, %v, want both 7.0", val1, val2) + } +} + +func TestOperationsSwap(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(1.0) + s.Push(2.0) + + err := o.Swap(s) + if err != nil { + t.Fatalf("Swap() returned error: %v", err) + } + + val1, _ := s.Pop() + val2, _ := s.Pop() + if val1 != 1.0 || val2 != 2.0 { + t.Errorf("After Swap, values = %v, %v, want 1.0, 2.0 (swapped)", val1, val2) + } +} + +func TestOperationsSwapInsufficient(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(5.0) + + err := o.Swap(s) + if err == nil { + t.Error("Swap with insufficient operands should return error") + } +} + +func TestOperationsPop(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(1.0) + s.Push(2.0) + s.Push(3.0) + + err := o.Pop(s) + if err != nil { + t.Fatalf("Pop() returned error: %v", err) + } + + if s.Len() != 2 { + t.Errorf("Length after Pop() = %d, want 2", s.Len()) + } +} + +func TestOperationsPopEmpty(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + + err := o.Pop(s) + if err == nil { + t.Error("Pop on empty stack should return error") + } +} + +func TestOperationsShow(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(1.0) + s.Push(2.0) + s.Push(3.0) + + result, err := o.Show(s) + if err != nil { + t.Fatalf("Show() returned error: %v", err) + } + + if result != "1 2 3" { + t.Errorf("Show() = %q, want \"1 2 3\"", result) + } +} + +func TestOperationsShowEmpty(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + + result, err := o.Show(s) + if err != nil { + t.Fatalf("Show() on empty stack returned error: %v", err) + } + + if !strings.Contains(result, "Stack is empty") { + t.Errorf("Show() on empty stack = %q, should contain 'Stack is empty'", result) + } +} + +func TestOperationsAssignVariable(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + s.Push(5.0) + + err := o.AssignVariable(s, "x") + if err != nil { + t.Fatalf("AssignVariable() returned error: %v", err) + } + + val, exists := v.GetVariable("x") + if !exists { + t.Error("Variable x should exist after assignment") + } + if val != 5.0 { + t.Errorf("Variable x value = %v, want 5.0", val) + } + + // Verify value was popped from stack + if s.Len() != 0 { + t.Errorf("Stack length after assignment = %d, want 0", s.Len()) + } +} + +func TestOperationsAssignVariableEmptyName(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + + err := o.AssignVariable(s, "") + if err == nil { + t.Error("AssignVariable with empty name should return error") + } +} + +func TestOperationsUseVariable(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + + v.SetVariable("pi", 3.14159) + + err := o.UseVariable(s, "pi") + if err != nil { + t.Fatalf("UseVariable() returned error: %v", err) + } + + val, err := s.Pop() + if err != nil { + t.Fatalf("Pop() after UseVariable() returned error: %v", err) + } + if val != 3.14159 { + t.Errorf("Variable value pushed to stack = %v, want 3.14159", val) + } +} + +func TestOperationsUseVariableUndefined(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + + err := o.UseVariable(s, "undefined") + if err == nil { + t.Error("UseVariable for undefined variable should return error") + } + if !strings.Contains(err.Error(), "undefined variable") { + t.Errorf("UseVariable error = %v, should contain 'undefined variable'", err) + } +} + +func TestOperationsDeleteVariable(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + + v.SetVariable("temp", 100.0) + + err := o.DeleteVariable("temp") + if err != nil { + t.Fatalf("DeleteVariable() returned error: %v", err) + } + + _, exists := v.GetVariable("temp") + if exists { + t.Error("Variable should not exist after deletion") + } +} + +func TestOperationsDeleteVariableUndefined(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + + err := o.DeleteVariable("nonexistent") + if err == nil { + t.Error("DeleteVariable for undefined variable should return error") + } +} + +func TestOperationsListVariables(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + + v.SetVariable("x", 1.0) + v.SetVariable("y", 2.0) + + result, err := o.ListVariables() + if err != nil { + t.Fatalf("ListVariables() returned error: %v", err) + } + + if strings.Contains(result, "No variables defined") { + t.Error("ListVariables should show variables, not 'No variables defined'") + } + if !strings.Contains(result, "x") || !strings.Contains(result, "y") { + t.Errorf("ListVariables output should contain all variable names, got: %s", result) + } +} + +func TestOperationsClearVariables(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + + v.SetVariable("x", 1.0) + v.SetVariable("y", 2.0) + + o.ClearVariables() + + if v.Count() != 0 { + t.Errorf("Count after ClearVariables() = %d, want 0", v.Count()) + } +} + +func TestOperationsConcurrent(t *testing.T) { + v := NewVariables().(*Variables) + o := NewOperations(v) + s := NewStack() + + // Test concurrent variable access + done := make(chan bool, 10) + for i := 0; i < 5; i++ { + go func(id int) { + name := fmt.Sprintf("concurrent%d", id) + s.Push(float64(id)) + o.AssignVariable(s, name) + done <- true + }(i) + } + + for i := 0; i < 5; i++ { + <-done + } + + if v.Count() != 5 { + t.Errorf("Final count = %d, want 5", v.Count()) + } +} diff --git a/internal/rpn/variables.go b/internal/rpn/variables.go index 39d9f50..9356a07 100644 --- a/internal/rpn/variables.go +++ b/internal/rpn/variables.go @@ -141,3 +141,58 @@ func (v *Variables) HasVariable(name string) bool { _, exists := v.variables[name] return exists } + +// Stack represents a simple float64 stack for RPN calculations. +type Stack struct { + values []float64 +} + +// NewStack creates a new empty stack. +func NewStack() *Stack { + return &Stack{ + values: make([]float64, 0), + } +} + +// Push adds a value to the top of the stack. +func (s *Stack) Push(val float64) { + s.values = append(s.values, val) +} + +// Pop removes and returns the top value from the stack. +// Returns an error if the stack is empty. +func (s *Stack) Pop() (float64, error) { + if len(s.values) == 0 { + return 0, fmt.Errorf("stack is empty") + } + + val := s.values[len(s.values)-1] + s.values = s.values[:len(s.values)-1] + return val, nil +} + +// Peek returns the top value without removing it. +// Returns an error if the stack is empty. +func (s *Stack) Peek() (float64, error) { + if len(s.values) == 0 { + return 0, fmt.Errorf("stack is empty") + } + return s.values[len(s.values)-1], nil +} + +// Len returns the number of values on the stack. +func (s *Stack) Len() int { + return len(s.values) +} + +// Values returns a copy of all stack values (top-to-bottom order). +func (s *Stack) Values() []float64 { + vals := make([]float64, len(s.values)) + copy(vals, s.values) + return vals +} + +// Clear removes all values from the stack. +func (s *Stack) Clear() { + s.values = s.values[:0] +} |
