summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-25 17:57:54 +0200
committerPaul Buetow <paul@buetow.org>2026-03-25 17:57:54 +0200
commita3d3b676796f93f41f5b44b1d2b86b15f99080a0 (patch)
treef8d6182cf95ca3816f9bbba251db0ce1ef77a802
parent31353ea7a3cb2f5ec5d14adcfaff840222185ae7 (diff)
Fix Ln operation and add comprehensive tests
- Fixed Ln operation to handle Value conversion before math.Log using Float64() which handles boolean conversion (true → 1, false → 0) - Added TestLnWithBoolean and TestLnEdgeCases tests for comprehensive coverage - Refactored operations.go into separate files (arithmetic.go, boolean_ops.go, hyper.go, stack.go, variable.go) - Removed unused toNumber function from number.go - Added Float64() method to Value struct for boolean conversion
-rw-r--r--internal/rpn/arithmetic.go186
-rw-r--r--internal/rpn/boolean_ops.go113
-rw-r--r--internal/rpn/boolean_test.go20
-rw-r--r--internal/rpn/hyper.go304
-rw-r--r--internal/rpn/number.go17
-rw-r--r--internal/rpn/operations.go20
-rw-r--r--internal/rpn/operations_test.go279
-rw-r--r--internal/rpn/stack.go68
-rw-r--r--internal/rpn/variable.go72
-rw-r--r--internal/rpn/variables.go20
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
}