diff options
| -rw-r--r-- | internal/rpn/arithmetic.go | 186 | ||||
| -rw-r--r-- | internal/rpn/boolean_ops.go | 113 | ||||
| -rw-r--r-- | internal/rpn/boolean_test.go | 20 | ||||
| -rw-r--r-- | internal/rpn/hyper.go | 304 | ||||
| -rw-r--r-- | internal/rpn/number.go | 17 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 20 | ||||
| -rw-r--r-- | internal/rpn/operations_test.go | 279 | ||||
| -rw-r--r-- | internal/rpn/stack.go | 68 | ||||
| -rw-r--r-- | internal/rpn/variable.go | 72 | ||||
| -rw-r--r-- | internal/rpn/variables.go | 20 |
10 files changed, 1061 insertions, 38 deletions
diff --git a/internal/rpn/arithmetic.go b/internal/rpn/arithmetic.go new file mode 100644 index 0000000..049cf25 --- /dev/null +++ b/internal/rpn/arithmetic.go @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Paul Buetow + +package rpn + +import ( + "fmt" + "math" +) + +// ArithmeticOperations provides arithmetic operator implementations. +type ArithmeticOperations struct { + mode CalculationMode +} + +// NewArithmeticOperations creates a new ArithmeticOperations instance. +func NewArithmeticOperations(mode CalculationMode) *ArithmeticOperations { + return &ArithmeticOperations{mode: mode} +} + +// Add pops two values from stack, adds them, and pushes result. +func (o *ArithmeticOperations) Add(stack *Stack) error { + bVal, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for +: %w", err) + } + + aVal, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for +: %w", err) + } + + // Use the Number interface for arithmetic + stack.Push(aVal.Add(bVal)) + return nil +} + +// Subtract pops two values from stack, subtracts (a - b), and pushes result. +func (o *ArithmeticOperations) 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.Sub(b)) + return nil +} + +// Multiply pops two values from stack, multiplies them, and pushes result. +func (o *ArithmeticOperations) 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.Mul(b)) + return nil +} + +// Divide pops two values from stack, divides (a / b), and pushes result. +func (o *ArithmeticOperations) Divide(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for /: %w", err) + } + + if b.IsZero() { + return fmt.Errorf("division by zero") + } + + a, err2 := stack.Pop() + if err2 != nil { + return fmt.Errorf("insufficient operands for /: %w", err2) + } + + result, err2 := a.Div(b) + if err2 != nil { + return fmt.Errorf("division error: %w", err2) + } + stack.Push(result) + return nil +} + +// Power pops two values from stack, raises first to power of second (a ^ b), and pushes result. +func (o *ArithmeticOperations) 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(a.Pow(b)) + return nil +} + +// Modulo pops two values from stack, computes modulo (a % b), and pushes result. +func (o *ArithmeticOperations) 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.IsZero() { + return fmt.Errorf("modulo by zero") + } + + result, err := a.Mod(b) + if err != nil { + return fmt.Errorf("modulo error: %w", err) + } + stack.Push(result) + return nil +} + +// Log2 pops one value from stack, computes log base 2 (log₂(a)), and pushes result. +func (o *ArithmeticOperations) Log2(stack *Stack) error { + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for lg: %w", err) + } + + // Check if value is zero or negative + val := a.Float64() + if val <= 0 { + return fmt.Errorf("log2 undefined for non-positive numbers") + } + + // Compute log2 using the number interface + stack.Push(NewNumber(math.Log2(val), o.mode)) + return nil +} + +// Log10 pops one value from stack, computes log base 10 (log₁₀(a)), and pushes result. +func (o *ArithmeticOperations) Log10(stack *Stack) error { + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for log: %w", err) + } + + // Check if value is zero or negative + val := a.Float64() + if val <= 0 { + return fmt.Errorf("log10 undefined for non-positive numbers") + } + + // Compute log10 using the number interface + stack.Push(NewNumber(math.Log10(val), o.mode)) + return nil +} + +// Ln pops one value from stack, computes natural log (ln(a)), and pushes result. +func (o *ArithmeticOperations) Ln(stack *Stack) error { + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for ln: %w", err) + } + + // Check if value is zero or negative + val := a.Float64() + if val <= 0 { + return fmt.Errorf("ln undefined for non-positive numbers") + } + + // Compute ln using the number interface + stack.Push(NewNumber(math.Log(val), o.mode)) + return nil +} diff --git a/internal/rpn/boolean_ops.go b/internal/rpn/boolean_ops.go new file mode 100644 index 0000000..d07aa26 --- /dev/null +++ b/internal/rpn/boolean_ops.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Paul Buetow + +package rpn + +import ( + "fmt" +) + +// BooleanOperations provides boolean comparison operator implementations. +type BooleanOperations struct { +} + +// NewBooleanOperations creates a new BooleanOperations instance. +func NewBooleanOperations() *BooleanOperations { + return &BooleanOperations{} +} + +// GT pops two values from stack, compares (a > b), and pushes a boolean result. +func (o *BooleanOperations) GT(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for gt: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for gt: %w", err) + } + + stack.Push(NewFloatFromBool(a.Float64() > b.Float64())) + return nil +} + +// LT pops two values from stack, compares (a < b), and pushes a boolean result. +func (o *BooleanOperations) LT(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for lt: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for lt: %w", err) + } + + stack.Push(NewFloatFromBool(a.Float64() < b.Float64())) + return nil +} + +// GTE pops two values from stack, compares (a >= b), and pushes a boolean result. +func (o *BooleanOperations) GTE(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for gte: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for gte: %w", err) + } + + stack.Push(NewFloatFromBool(a.Float64() >= b.Float64())) + return nil +} + +// LTE pops two values from stack, compares (a <= b), and pushes a boolean result. +func (o *BooleanOperations) LTE(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for lte: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for lte: %w", err) + } + + stack.Push(NewFloatFromBool(a.Float64() <= b.Float64())) + return nil +} + +// EQ pops two values from stack, compares (a == b), and pushes a boolean result. +func (o *BooleanOperations) EQ(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for eq: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for eq: %w", err) + } + + stack.Push(NewFloatFromBool(a.Float64() == b.Float64())) + return nil +} + +// NEQ pops two values from stack, compares (a != b), and pushes a boolean result. +func (o *BooleanOperations) NEQ(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for neq: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for neq: %w", err) + } + + stack.Push(NewFloatFromBool(a.Float64() != b.Float64())) + return nil +} diff --git a/internal/rpn/boolean_test.go b/internal/rpn/boolean_test.go index 02c5a16..f0ced89 100644 --- a/internal/rpn/boolean_test.go +++ b/internal/rpn/boolean_test.go @@ -239,29 +239,29 @@ func TestMixedBooleanNumericArithmetic(t *testing.T) { // TestBooleanShowFormat tests that Show command displays boolean values as true/false func TestBooleanShowFormat(t *testing.T) { tests := []struct { - name string + name string expression string - expected string + expected string }{ { - name: "show true", + name: "show true", expression: "true show", - expected: "true", + expected: "true", }, { - name: "show false", + name: "show false", expression: "false show", - expected: "false", + expected: "false", }, { - name: "show mixed stack", + name: "show mixed stack", expression: "1 true 2 show", - expected: "1 true 2", + expected: "1 true 2", }, { - name: "show comparison result", + name: "show comparison result", expression: "5 3 gt show", - expected: "true", + expected: "true", }, } diff --git a/internal/rpn/hyper.go b/internal/rpn/hyper.go new file mode 100644 index 0000000..58a0ca2 --- /dev/null +++ b/internal/rpn/hyper.go @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Paul Buetow + +package rpn + +import ( + "fmt" + "math" +) + +// HyperOperations provides hyper operator implementations. +type HyperOperations struct { + mode CalculationMode +} + +// NewHyperOperations creates a new HyperOperations instance. +func NewHyperOperations(mode CalculationMode) *HyperOperations { + return &HyperOperations{mode: mode} +} + +// HyperAdd pops all values from stack, sums them, and pushes result. +func (o *HyperOperations) 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 []Number + 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 with Number interface + sum := 0.0 + for i := 0; i < len(values); i++ { + sum += values[i].Float64() + } + stack.Push(NewNumber(sum, o.mode)) + return nil +} + +// HyperMultiply pops all values from stack, multiplies them left-associative, and pushes result. +func (o *HyperOperations) 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.Float64() + } + stack.Push(NewNumber(product, o.mode)) + return nil +} + +// HyperSubtract pops all values from stack, subtracts them left-associative, and pushes result. +func (o *HyperOperations) 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 []Number + 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 with Number interface + result := values[0].Float64() + for i := 1; i < len(values); i++ { + result -= values[i].Float64() + } + stack.Push(NewNumber(result, o.mode)) + return nil +} + +// HyperDivide pops all values from stack, divides them left-associative, and pushes result. +func (o *HyperOperations) 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 []Number + 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 with Number interface + result := values[0].Float64() + for i := 1; i < len(values); i++ { + val := values[i].Float64() + if val == 0 { + return fmt.Errorf("division by zero") + } + result /= val + } + stack.Push(NewNumber(result, o.mode)) + return nil +} + +// HyperPower pops all values from stack, raises to power left-associative, and pushes result. +func (o *HyperOperations) 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 []Number + 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 with Number interface + result := values[0].Float64() + for i := 1; i < len(values); i++ { + result = math.Pow(result, values[i].Float64()) + } + stack.Push(NewNumber(result, o.mode)) + return nil +} + +// HyperModulo pops all values from stack, computes modulo left-associative, and pushes result. +func (o *HyperOperations) 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 []Number + 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 with Number interface + result := values[0].Float64() + for i := 1; i < len(values); i++ { + val := values[i].Float64() + if val == 0 { + return fmt.Errorf("modulo by zero") + } + result = math.Mod(result, val) + } + stack.Push(NewNumber(result, o.mode)) + return nil +} + +// HyperLog2 pops all values from stack, computes sum of log2 for all values, and pushes result. +// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product). +func (o *HyperOperations) HyperLog2(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperlog2: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []Number + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperlog2: %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] + } + + // Sum the log2 of all values with Number interface + var result float64 = 0 + for i := 0; i < len(values); i++ { + val := values[i].Float64() + if val <= 0 { + return fmt.Errorf("hyperlog2 undefined for non-positive numbers") + } + result += math.Log2(val) + } + + // Push the result as a Number + stack.Push(NewNumber(result, o.mode)) + return nil +} + +// HyperLog10 pops all values from stack, computes sum of log10 for all values, and pushes result. +// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product). +func (o *HyperOperations) HyperLog10(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperlog10: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []Number + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperlog10: %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] + } + + // Sum the log10 of all values + var result float64 = 0 + for i := 0; i < len(values); i++ { + val := values[i].Float64() + if val <= 0 { + return fmt.Errorf("hyperlog10 undefined for non-positive numbers") + } + result += math.Log10(val) + } + + // Push the result as a Number + stack.Push(NewNumber(result, o.mode)) + return nil +} + +// HyperLn pops all values from stack, computes sum of natural log for all values, and pushes result. +// This follows the same pattern as HyperAdd (sum) and HyperMultiply (product). +func (o *HyperOperations) HyperLn(stack *Stack) error { + if stack.Len() < 2 { + return fmt.Errorf("insufficient operands for hyperln: need at least 2 values") + } + + // Pop all values into a slice (in reverse order - top first) + var values []Number + for stack.Len() > 0 { + val, err := stack.Pop() + if err != nil { + return fmt.Errorf("hyperln: %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] + } + + // Sum the natural log of all values with Number interface + var result float64 = 0 + for i := 0; i < len(values); i++ { + val := values[i].Float64() + if val <= 0 { + return fmt.Errorf("hyperln undefined for non-positive numbers") + } + result += math.Log(val) + } + stack.Push(NewNumber(result, o.mode)) + return nil +} diff --git a/internal/rpn/number.go b/internal/rpn/number.go index 9cf9216..c25b483 100644 --- a/internal/rpn/number.go +++ b/internal/rpn/number.go @@ -9,23 +9,6 @@ import ( "math/big" ) -// toNumber converts a Value to float64. -// If the value is a boolean, true returns 1 and false returns 0. -// If the value is a number, it returns the numeric value directly. -// This enables automatic coercion of booleans to numbers in arithmetic operations. -// -// v: the Value to convert -// Returns the float64 representation -func toNumber(v Value) float64 { - if v.isBool { - if v.boolVal { - return 1 - } - return 0 - } - return v.numVal -} - // Number represents a number that can be used in RPN calculations. // It can be either a float64 or a *big.Rat for precise rational calculations. // Booleans are also supported through IsBool() and Bool() methods. diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index 1c9182a..1300d99 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -340,7 +340,8 @@ func (o *Operations) Log2(stack *Stack) error { return fmt.Errorf("insufficient operands for lg: %w", err) } - // Check if value is zero or negative + // Use Float64() to convert value to float64, handling boolean values: + // - true → 1, false → 0 val := a.Float64() if val <= 0 { return fmt.Errorf("log2 undefined for non-positive numbers") @@ -358,7 +359,8 @@ func (o *Operations) Log10(stack *Stack) error { return fmt.Errorf("insufficient operands for log: %w", err) } - // Check if value is zero or negative + // Use Float64() to convert value to float64, handling boolean values: + // - true → 1, false → 0 val := a.Float64() if val <= 0 { return fmt.Errorf("log10 undefined for non-positive numbers") @@ -376,7 +378,8 @@ func (o *Operations) Ln(stack *Stack) error { return fmt.Errorf("insufficient operands for ln: %w", err) } - // Check if value is zero or negative + // Use Float64() to convert value to float64, handling boolean values: + // - true → 1, false → 0 val := a.Float64() if val <= 0 { return fmt.Errorf("ln undefined for non-positive numbers") @@ -587,7 +590,8 @@ func (o *Operations) HyperLog2(stack *Stack) error { values[i], values[j] = values[j], values[i] } - // Sum the log2 of all values with Number interface + // Sum the log2 of all values using Float64() for value conversion: + // - true → 1, false → 0 var result float64 = 0 for i := 0; i < len(values); i++ { val := values[i].Float64() @@ -624,7 +628,8 @@ func (o *Operations) HyperLog10(stack *Stack) error { values[i], values[j] = values[j], values[i] } - // Sum the log10 of all values + // Sum the log10 of all values using Float64() for value conversion: + // - true → 1, false → 0 var result float64 = 0 for i := 0; i < len(values); i++ { val := values[i].Float64() @@ -661,7 +666,8 @@ func (o *Operations) HyperLn(stack *Stack) error { values[i], values[j] = values[j], values[i] } - // Sum the natural log of all values with Number interface + // Sum the natural log of all values using Float64() for value conversion: + // - true → 1, false → 0 var result float64 = 0 for i := 0; i < len(values); i++ { val := values[i].Float64() @@ -830,7 +836,7 @@ func (o *Operations) Show(stack *Stack) (string, error) { if i > 0 { result += " " } - // Use Value.String() to format values correctly: + // Use val.String() to format values correctly: // - Boolean values show as "true"/"false" // - Number values show with appropriate precision result += val.String() diff --git a/internal/rpn/operations_test.go b/internal/rpn/operations_test.go index dc70ebe..0e52bb0 100644 --- a/internal/rpn/operations_test.go +++ b/internal/rpn/operations_test.go @@ -668,6 +668,205 @@ func TestLn(t *testing.T) { } } +func TestLog2WithBoolean(t *testing.T) { + o := NewOperations(NewVariables()) + stack := NewStack() + + // Test with boolean true (should be converted to 1, log₂(1) = 0) + stack.Push(NewFloatFromBool(true)) + err := o.Log2(stack) + if err != nil { + t.Errorf("Log2(true) returned error: %v", err) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if val.Float64() != 0.0 { + t.Errorf("Log2(true) = %f, want 0.0 (log₂(1) = 0)", val.Float64()) + } + + // Test with boolean false (should be converted to 0, log₂(0) should error) + stack.Push(NewFloatFromBool(false)) + err = o.Log2(stack) + if err == nil { + t.Errorf("Log2(false) should return error for log₂(0), got nil") + } +} + +func TestLog10WithBoolean(t *testing.T) { + o := NewOperations(NewVariables()) + stack := NewStack() + + // Test with boolean true (should be converted to 1, log₁₀(1) = 0) + stack.Push(NewFloatFromBool(true)) + err := o.Log10(stack) + if err != nil { + t.Errorf("Log10(true) returned error: %v", err) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if val.Float64() != 0.0 { + t.Errorf("Log10(true) = %f, want 0.0 (log₁₀(1) = 0)", val.Float64()) + } + + // Test with boolean false (should be converted to 0, log₁₀(0) should error) + stack.Push(NewFloatFromBool(false)) + err = o.Log10(stack) + if err == nil { + t.Errorf("Log10(false) should return error for log₁₀(0), got nil") + } +} + +func TestLnWithBoolean(t *testing.T) { + o := NewOperations(NewVariables()) + stack := NewStack() + + // Test with boolean true (should be converted to 1, ln(1) = 0) + stack.Push(NewFloatFromBool(true)) + err := o.Ln(stack) + if err != nil { + t.Errorf("Ln(true) returned error: %v", err) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if val.Float64() != 0.0 { + t.Errorf("Ln(true) = %f, want 0.0 (ln(1) = 0)", val.Float64()) + } + + // Test with boolean false (should be converted to 0, ln(0) should error) + stack.Push(NewFloatFromBool(false)) + err = o.Ln(stack) + if err == nil { + t.Errorf("Ln(false) should return error for ln(0), got nil") + } +} + +func TestLnEdgeCases(t *testing.T) { + o := NewOperations(NewVariables()) + stack := NewStack() + + // Test ln(negative) should error + stack.Push(NewNumber(-1.0, FloatMode)) + err := o.Ln(stack) + if err == nil { + t.Errorf("Ln(-1) should return error, got nil") + } + + // Test ln(0) should error + stack.Push(NewNumber(0.0, FloatMode)) + err = o.Ln(stack) + if err == nil { + t.Errorf("Ln(0) should return error, got nil") + } + + // Test ln(very small positive) should work + stack.Push(NewNumber(0.001, FloatMode)) + err = o.Ln(stack) + if err != nil { + t.Errorf("Ln(0.001) should not return error, got: %v", err) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if val.Float64() > -6.0 || val.Float64() < -7.0 { + t.Errorf("Ln(0.001) = %f, want ~-6.9 (ln(0.001))", val.Float64()) + } +} + +func TestHyperLog2WithBoolean(t *testing.T) { + o := NewOperations(NewVariables()) + stack := NewStack() + + // Test hyperlog₂(4, true) = log₂(4) + log₂(1) = 2 + 0 = 2 + // true should be converted to 1 + stack.Push(NewNumber(4.0, FloatMode)) + stack.Push(NewFloatFromBool(true)) + err := o.HyperLog2(stack) + if err != nil { + t.Errorf("HyperLog2(4, true) returned error: %v", err) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if val.Float64() != 2.0 { + t.Errorf("HyperLog2(4, true) = %f, want 2.0 (log₂(4) + log₂(1) = 2 + 0)", val.Float64()) + } + + // Test hyperlog₂(4, false) = log₂(4) + log₂(0) should error + // false should be converted to 0, which is undefined for log₂ + stack.Push(NewNumber(4.0, FloatMode)) + stack.Push(NewFloatFromBool(false)) + err = o.HyperLog2(stack) + if err == nil { + t.Errorf("HyperLog2(4, false) should return error for log₂(0), got nil") + } +} + +func TestHyperLog10WithBoolean(t *testing.T) { + o := NewOperations(NewVariables()) + stack := NewStack() + + // Test hyperlog₁₀(10, true) = log₁₀(10) + log₁₀(1) = 1 + 0 = 1 + // true should be converted to 1 + stack.Push(NewNumber(10.0, FloatMode)) + stack.Push(NewFloatFromBool(true)) + err := o.HyperLog10(stack) + if err != nil { + t.Errorf("HyperLog10(10, true) returned error: %v", err) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if val.Float64() != 1.0 { + t.Errorf("HyperLog10(10, true) = %f, want 1.0 (log₁₀(10) + log₁₀(1) = 1 + 0)", val.Float64()) + } + + // Test hyperlog₁₀(10, false) = log₁₀(10) + log₁₀(0) should error + stack.Push(NewNumber(10.0, FloatMode)) + stack.Push(NewFloatFromBool(false)) + err = o.HyperLog10(stack) + if err == nil { + t.Errorf("HyperLog10(10, false) should return error for log₁₀(0), got nil") + } +} + +func TestHyperLnWithBoolean(t *testing.T) { + o := NewOperations(NewVariables()) + stack := NewStack() + + // Test hyperln(e, true) = ln(e) + ln(1) = 1 + 0 = 1 + // true should be converted to 1 + stack.Push(NewNumber(math.E, FloatMode)) + stack.Push(NewFloatFromBool(true)) + err := o.HyperLn(stack) + if err != nil { + t.Errorf("HyperLn(e, true) returned error: %v", err) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if math.Abs(val.Float64()-1.0) > 0.0001 { + t.Errorf("HyperLn(e, true) = %f, want ~1.0 (ln(e) + ln(1) = 1 + 0)", val.Float64()) + } + + // Test hyperln(e, false) = ln(e) + ln(0) should error + stack.Push(NewNumber(math.E, FloatMode)) + stack.Push(NewFloatFromBool(false)) + err = o.HyperLn(stack) + if err == nil { + t.Errorf("HyperLn(e, false) should return error for ln(0), got nil") + } +} + func TestHyperLog2(t *testing.T) { o := NewOperations(NewVariables()) stack := NewStack() @@ -721,7 +920,7 @@ func TestHyperLn(t *testing.T) { // Test hyperln(e, e²) = ln(e) + ln(e²) = 1 + 2 = 3 stack.Push(NewNumber(math.E, FloatMode)) - stack.Push(NewNumber(math.E * math.E, FloatMode)) + stack.Push(NewNumber(math.E*math.E, FloatMode)) err := o.HyperLn(stack) if err != nil { t.Errorf("HyperLn() returned error: %v", err) @@ -734,3 +933,81 @@ func TestHyperLn(t *testing.T) { t.Errorf("HyperLn(e, e²) = %f, want ~3.0", val.Float64()) } } + +func TestOperatorRegistry(t *testing.T) { + o := NewOperations(NewVariables()) + registry := NewOperatorRegistry(o) + + // Test IsStandardOperator with valid operators + validOperators := []string{"+", "-", "*", "/", "^", "%", "lg", "log", "ln", "gt", "lt", "gte", "lte", "eq", "neq", "dup", "swap", "pop", "show", "showstack", "print", "vars", "clear"} + for _, op := range validOperators { + if !registry.IsStandardOperator(op) { + t.Errorf("IsStandardOperator(%q) = false, want true", op) + } + } + + // Test IsStandardOperator with invalid operators + invalidOperators := []string{"invalid", "xyz", "123"} + for _, op := range invalidOperators { + if registry.IsStandardOperator(op) { + t.Errorf("IsStandardOperator(%q) = true, want false", op) + } + } + + // Test IsHyperOperator with valid operators + hyperOperators := []string{"[+]", "[-]", "[*]", "[/]", "[^]", "[%]", "[lg]", "[log]", "[ln]"} + for _, op := range hyperOperators { + if !registry.IsHyperOperator(op) { + t.Errorf("IsHyperOperator(%q) = false, want true", op) + } + } + + // Test IsHyperOperator with invalid operators + for _, op := range invalidOperators { + if registry.IsHyperOperator(op) { + t.Errorf("IsHyperOperator(%q) = true, want false", op) + } + } +} + +func TestOperatorRegistryHandleStandardOperator(t *testing.T) { + o := NewOperations(NewVariables()) + registry := NewOperatorRegistry(o) + stack := NewStack() + + // Test standard operator handling + testCases := []struct { + name string + token string + prepare func() + expected float64 + }{ + {"Addition", "+", func() { stack.Push(NewNumber(3.0, FloatMode)); stack.Push(NewNumber(4.0, FloatMode)) }, 7.0}, + {"Subtraction", "-", func() { stack.Push(NewNumber(10.0, FloatMode)); stack.Push(NewNumber(4.0, FloatMode)) }, 6.0}, + {"Multiplication", "*", func() { stack.Push(NewNumber(5.0, FloatMode)); stack.Push(NewNumber(3.0, FloatMode)) }, 15.0}, + {"Division", "/", func() { stack.Push(NewNumber(20.0, FloatMode)); stack.Push(NewNumber(4.0, FloatMode)) }, 5.0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.prepare() + result, handled, err := registry.HandleStandardOperator(stack, tc.token) + if err != nil { + t.Errorf("HandleStandardOperator(%q) returned error: %v", tc.token, err) + } + if !handled { + t.Errorf("HandleStandardOperator(%q) = false, want true", tc.token) + } + if result != "" { + t.Errorf("HandleStandardOperator(%q) returned non-empty result: %q", tc.token, result) + } + val, err := stack.Pop() + if err != nil { + t.Errorf("Pop() returned error: %v", err) + } + if val.Float64() != tc.expected { + t.Errorf("Result = %f, want %f", val.Float64(), tc.expected) + } + }) + } +} diff --git a/internal/rpn/stack.go b/internal/rpn/stack.go new file mode 100644 index 0000000..a956902 --- /dev/null +++ b/internal/rpn/stack.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Paul Buetow + +package rpn + +import ( + "fmt" +) + +// StackOperations provides stack manipulation operator implementations. +type StackOperations struct { +} + +// NewStackOperations creates a new StackOperations instance. +func NewStackOperations() *StackOperations { + return &StackOperations{} +} + +// Dup duplicates the top stack value. +func (o *StackOperations) 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 *StackOperations) Swap(stack *Stack) error { + b, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for swap: %w", err) + } + + a, err := stack.Pop() + if err != nil { + return fmt.Errorf("insufficient operands for swap: %w", err) + } + + // Push in swapped order + stack.Push(b) + stack.Push(a) + return nil +} + +// Pop removes the top stack value. +func (o *StackOperations) 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 state as a string without modifying it. +func (o *StackOperations) Show(stack *Stack) (string, error) { + if stack.Len() == 0 { + return "", fmt.Errorf("empty stack") + } + // For now, just return the top value as a string + // In a full implementation, this would show the entire stack + val, err := stack.Peek() + if err != nil { + return "", err + } + return val.String(), nil +} diff --git a/internal/rpn/variable.go b/internal/rpn/variable.go new file mode 100644 index 0000000..f3302df --- /dev/null +++ b/internal/rpn/variable.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Paul Buetow + +package rpn + +import ( + "fmt" +) + +// VariableOperations provides variable management operator implementations. +type VariableOperations struct { + vars VariableStore +} + +// NewVariableOperations creates a new VariableOperations instance. +func NewVariableOperations(vars VariableStore) *VariableOperations { + return &VariableOperations{vars: vars} +} + +// AssignVariable assigns a value from the stack to a variable. +// Usage: `name value =` +func (o *VariableOperations) AssignVariable(stack *Stack, name string) error { + val, err := stack.Pop() + if err != nil { + return err + } + + // Convert Number to float64 for variable storage + return o.vars.SetVariable(name, val.Float64()) +} + +// UseVariable pushes a variable's value onto the stack. +// Usage: `varname` (pushes stored value) +func (o *VariableOperations) 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("%w: %s", ErrVariableNotFound, name) + } + + stack.Push(NewNumber(val, FloatMode)) + return nil +} + +// DeleteVariable removes a variable. +// Usage: `name d` +func (o *VariableOperations) DeleteVariable(name string) error { + if name == "" { + return fmt.Errorf("variable name cannot be empty") + } + + deleted := o.vars.DeleteVariable(name) + if !deleted { + return fmt.Errorf("%w: %s", ErrVariableNotFound, name) + } + return nil +} + +// ListVariables returns a string listing all variables. +// Usage: `vars` +func (o *VariableOperations) ListVariables() (string, error) { + return o.vars.FormatVariables(), nil +} + +// ClearVariables removes all variables. +// Usage: `clear` +func (o *VariableOperations) ClearVariables() { + o.vars.ClearVariables() +} diff --git a/internal/rpn/variables.go b/internal/rpn/variables.go index c08d3a5..f2a6221 100644 --- a/internal/rpn/variables.go +++ b/internal/rpn/variables.go @@ -26,9 +26,9 @@ var ( // arithmetic expressions (e.g., "5 3 == 1 +" where "5 3 ==" produces false=0, // and "0 + 1" produces 1). type Value struct { - isBool bool + isBool bool boolVal bool - numVal float64 + numVal float64 } // NewNumberValue creates a new Value containing a float64 number. @@ -56,7 +56,21 @@ func (v Value) Bool() bool { return v.boolVal } -// Number returns the float64 value, or 0 if the value is not a number. +// Float64 returns the float64 value. +// If the value is a boolean, true returns 1 and false returns 0. +// If the value is a number, it returns the numeric value directly. +func (v Value) Float64() float64 { + if v.isBool { + if v.boolVal { + return 1 + } + return 0 + } + return v.numVal +} + +// Number returns the float64 value (deprecated, use Float64 instead). +// If the value is a boolean, this returns 0 (the numeric value is not used for booleans). func (v Value) Number() float64 { return v.numVal } |
