diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-20 22:22:34 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-20 22:22:34 +0200 |
| commit | 8fb02e7bfc0669fea2adb5d38af2a8a12a1eb6b5 (patch) | |
| tree | 5ac5faf8f9e660d8e76e25806ab235ecb30bac01 /internal | |
| parent | 59d8038a2188319640863ecad33ea855b2cf3c0e (diff) | |
feat: Add hyper operator methods for stack operations
- Added HyperAdd, HyperMultiply, HyperSubtract, HyperDivide, HyperPower, HyperModulo
- Each hyper operator pops all values from stack and applies left-associative operation
- Updated token evaluation to recognize [op] pattern (e.g., [+], [-], [*], [/], [^], [%])
- Added comprehensive test cases for all hyper operators
- Updated README.md with hyper operator documentation and fixed persistent RPN state notes
Hyper operators allow operating on all stack values at once, while regular operators
work on the top two values.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/rpn/operations.go | 176 | ||||
| -rw-r--r-- | internal/rpn/rpn.go | 24 | ||||
| -rw-r--r-- | internal/rpn/rpn_test.go | 152 |
3 files changed, 352 insertions, 0 deletions
diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index cbcc8fe..2f4b84c 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -123,6 +123,182 @@ func (o *Operations) Modulo(stack *Stack) error { return nil } +// Hyper operators - operate on all values on the stack + +// HyperAdd pops all values from stack, adds them left-associative, and pushes result. +func (o *Operations) HyperAdd(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperadd: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperadd: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + sum := 0.0 + for i := 0; i < len(values); i++ { + sum += values[i] + } + stack.Push(sum) + return nil +} + +// HyperMultiply pops all values from stack, multiplies them left-associative, and pushes result. +func (o *Operations) HyperMultiply(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hypermultiply: need at least 2 values") + } + + product := 1.0 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hypermultiply: %w", err) + } + product *= val + } + stack.Push(product) + return nil +} + +// HyperSubtract pops all values from stack, subtracts them left-associative, and pushes result. +func (o *Operations) HyperSubtract(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hypersubtract: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hypersubtract: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + result -= values[i] + } + stack.Push(result) + return nil +} + +// HyperDivide pops all values from stack, divides them left-associative, and pushes result. +func (o *Operations) HyperDivide(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperdivide: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperdivide: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + if values[i] == 0 { + return fmt.Errorf("division by zero") + } + result /= values[i] + } + stack.Push(result) + return nil +} + +// HyperPower pops all values from stack, raises to power left-associative, and pushes result. +func (o *Operations) HyperPower(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperpower: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperpower: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + result = math.Pow(result, values[i]) + } + stack.Push(result) + return nil +} + +// HyperModulo pops all values from stack, computes modulo left-associative, and pushes result. +func (o *Operations) HyperModulo(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hypermodulo: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []float64 + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hypermodulo: %w", err) + } + values = append(values, val) + } + + // Reverse to get left-to-right order (first pushed = first in) + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + // Process left-associative + result := values[0] + for i := 1; i < len(values); i++ { + if values[i] == 0 { + return fmt.Errorf("modulo by zero") + } + result = math.Mod(result, values[i]) + } + stack.Push(result) + return nil +} + // stack manipulation operators // Dup duplicates the top stack value. diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go index 07a5fb4..d21dd67 100644 --- a/internal/rpn/rpn.go +++ b/internal/rpn/rpn.go @@ -160,6 +160,30 @@ func (r *RPN) evaluate(tokens []string) (string, error) { if err := r.ops.Modulo(stack); err != nil { return "", fmt.Errorf("operator %%: %w", err) } + case "[+]": + if err := r.ops.HyperAdd(stack); err != nil { + return "", fmt.Errorf("hyperoperator [+]: %w", err) + } + case "[-]": + if err := r.ops.HyperSubtract(stack); err != nil { + return "", fmt.Errorf("hyperoperator [-]: %w", err) + } + case "[*]": + if err := r.ops.HyperMultiply(stack); err != nil { + return "", fmt.Errorf("hyperoperator [*]: %w", err) + } + case "[/]": + if err := r.ops.HyperDivide(stack); err != nil { + return "", fmt.Errorf("hyperoperator [/]: %w", err) + } + case "[^]": + if err := r.ops.HyperPower(stack); err != nil { + return "", fmt.Errorf("hyperoperator [^]: %w", err) + } + case "[%]": + if err := r.ops.HyperModulo(stack); err != nil { + return "", fmt.Errorf("hyperoperator [%%]: %w", err) + } case "dup": if err := r.ops.Dup(stack); err != nil { return "", fmt.Errorf("dup: %w", err) diff --git a/internal/rpn/rpn_test.go b/internal/rpn/rpn_test.go index aa285ac..412d3db 100644 --- a/internal/rpn/rpn_test.go +++ b/internal/rpn/rpn_test.go @@ -781,3 +781,155 @@ func TestRPNClearStack(t *testing.T) { t.Errorf("After clear = %q, want 'All variables cleared'", result) } } + +// Hyper operator tests + +func TestHyperAdd(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 1 2 3 4 5 [+] + result, err := r.ParseAndEvaluate("1 2 3 4 5 [+]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "15" { + t.Errorf("1 2 3 4 5 [+] = %q, want '15'", result) + } +} + +func TestHyperAddEdgeCases(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test with two values: 10 20 [+] + result, err := r.ParseAndEvaluate("10 20 [+]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "30" { + t.Errorf("10 20 [+] = %q, want '30'", result) + } + + // Test with single value should error - use fresh instance to avoid stack state + v2 := NewVariables().(*Variables) + r2 := NewRPN(v2) + _, err = r2.ParseAndEvaluate("5 [+]") + if err == nil { + t.Error("5 [+] should return error") + } +} + +func TestHyperSubtract(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 10 3 2 [-] => 10 - 3 - 2 = 5 + result, err := r.ParseAndEvaluate("10 3 2 [-]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "5" { + t.Errorf("10 3 2 [-] = %q, want '5'", result) + } +} + +func TestHyperMultiply(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 2 3 4 [*] => 2 * 3 * 4 = 24 + result, err := r.ParseAndEvaluate("2 3 4 [*]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "24" { + t.Errorf("2 3 4 [*] = %q, want '24'", result) + } +} + +func TestHyperDivide(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 100 5 2 [/] => 100 / 5 / 2 = 10 + result, err := r.ParseAndEvaluate("100 5 2 [/]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "10" { + t.Errorf("100 5 2 [/] = %q, want '10'", result) + } +} + +func TestHyperDivideByZero(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("100 0 [/]") + if err == nil { + t.Error("100 0 [/] should return error") + } +} + +func TestHyperPower(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 2 3 2 [^] => 2 ^ 3 ^ 2 = (2 ^ 3) ^ 2 = 8 ^ 2 = 64 + result, err := r.ParseAndEvaluate("2 3 2 [^]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "64" { + t.Errorf("2 3 2 [^] = %q, want '64'", result) + } +} + +func TestHyperModulo(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + // Test: 100 7 3 [%%] => 100 %% 7 %% 3 = 2 %% 3 = 2 + result, err := r.ParseAndEvaluate("100 7 3 [%]") + if err != nil { + t.Fatalf("ParseAndEvaluate failed: %v", err) + } + if result != "2" { + t.Errorf("100 7 3 [%%] = %q, want '2'", result) + } +} + +func TestHyperModuloByZero(t *testing.T) { + v := NewVariables().(*Variables) + r := NewRPN(v) + + _, err := r.ParseAndEvaluate("100 0 [%]") + if err == nil { + t.Error("100 0 [%] should return error") + } +} + +func TestHyperOperatorEdgeCases(t *testing.T) { + // Test with single value should error for all hyper operators + testCases := []struct { + input string + operands int + }{ + {"100 [%]", 1}, + {"5 [+]", 1}, + {"10 [-]", 1}, + {"2 [*]", 1}, + {"100 [/]", 1}, + {"2 [^]", 1}, + } + + for _, tc := range testCases { + v := NewVariables().(*Variables) + r := NewRPN(v) + _, err := r.ParseAndEvaluate(tc.input) + if err == nil { + t.Errorf("%s should return error for insufficient operands", tc.input) + } + } +} |
