summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-11 22:45:57 +0300
committerPaul Buetow <paul@buetow.org>2026-04-11 22:45:57 +0300
commitfe31559af818bcc1771b1a2eafc7f3aa1cac3f75 (patch)
tree192545c30e748de5a8bc96bf7d116e5971ee15f7
parenta1a55e2f1702b9f0f95ec10d3d9adcf35999f006 (diff)
feat: implement binary exponentiation with ** operator
- Add PowInt(int) method to Number interface - Implement binary exponentiation for Float and Rat types - Add FastPower operator to handle ** operator - Register ** operator in NewOperatorRegistry - Add unit tests for PowInt (basic, large, negative exponents) - Add integration tests for ** operator (CLI and stdin) - Update README.md with ** operator documentation
-rw-r--r--README.md13
-rw-r--r--integrationtests/cli_test.go95
-rw-r--r--internal/rpn/number.go88
-rw-r--r--internal/rpn/operations.go39
-rw-r--r--internal/rpn/operations_test.go92
5 files changed, 326 insertions, 1 deletions
diff --git a/README.md b/README.md
index ef97fdd..b722ddc 100644
--- a/README.md
+++ b/README.md
@@ -83,16 +83,27 @@ gt '5 6 *' # 5 * 6 = 30
gt '20 4 /' # 20 / 4 = 5
# → 5
-gt '2 3 ^' # 2^3 = 8
+gt '2 3 ^' # 2^3 = 8 (floating-point power)
# → 8
gt '2 10 **' # 2^10 = 1024 (Fast binary exponentiation)
# → 1024
+gt '2 -3 **' # 2^(-3) = 0.125 (negative integer exponent)
+# → 0.125
+
+gt '5 0 **' # 5^0 = 1 (zero exponent)
+# → 1
+
+gt '10 100 **' # 10^100 (very large exponent - efficient)
+# → 1e+100
+
gt '10 3 %' # 10 % 3 = 1 (modulo)
# → 1
```
+**Note:** The `**` operator uses binary exponentiation (exponentiation by squaring), which is more efficient than the general `^` operator for large integer exponents. It only works with integer exponents.
+
#### Expression Chaining
```bash
diff --git a/integrationtests/cli_test.go b/integrationtests/cli_test.go
index 8a0a076..fe62185 100644
--- a/integrationtests/cli_test.go
+++ b/integrationtests/cli_test.go
@@ -544,3 +544,98 @@ func TestCLIVariableAssignmentRepetition(t *testing.T) {
})
}
}
+
+// TestCLIBinaryExponentiation tests the ** operator with binary exponentiation.
+func TestCLIBinaryExponentiation(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ args []string
+ expected string
+ }{
+ {
+ name: "2 ** 10",
+ args: []string{"2", "10", "**"},
+ expected: "1024",
+ },
+ {
+ name: "3 ** 4",
+ args: []string{"3", "4", "**"},
+ expected: "81",
+ },
+ {
+ name: "2 ** -3",
+ args: []string{"2", "-3", "**"},
+ expected: "0.125",
+ },
+ {
+ name: "10 ** -2",
+ args: []string{"10", "-2", "**"},
+ expected: "0.01",
+ },
+ {
+ name: "5 ** 0",
+ args: []string{"5", "0", "**"},
+ expected: "1",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath, tt.args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
+
+// TestCLIBinaryExponentiationStdin tests the ** operator via stdin.
+func TestCLIBinaryExponentiationStdin(t *testing.T) {
+ binaryPath := buildBinary(t)
+
+ tests := []struct {
+ name string
+ stdin string
+ expected string
+ }{
+ {
+ name: "2 ** 10 via stdin",
+ stdin: "2 10 **",
+ expected: "1024",
+ },
+ {
+ name: "3 ** 4 via stdin",
+ stdin: "3 4 **",
+ expected: "81",
+ },
+ {
+ name: "2 ** -3 via stdin",
+ stdin: "2 -3 **",
+ expected: "0.125",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cmd := exec.Command(binaryPath)
+ cmd.Stdin = strings.NewReader(tt.stdin)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v\nOutput: %s", err, string(output))
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if !strings.Contains(outputStr, tt.expected) {
+ t.Errorf("output should contain '%s', got: %s", tt.expected, outputStr)
+ }
+ })
+ }
+}
diff --git a/internal/rpn/number.go b/internal/rpn/number.go
index 1dd71e0..9a41bc0 100644
--- a/internal/rpn/number.go
+++ b/internal/rpn/number.go
@@ -33,6 +33,9 @@ type Number interface {
// Pow returns this number raised to the power of another.
// Returns error if the operation is not supported (e.g., StringNum, Symbol).
Pow(other Number) (Number, error)
+ // PowInt returns this number raised to an integer power using binary exponentiation.
+ // Returns error if the operation is not supported (e.g., StringNum, Symbol).
+ PowInt(exp int) (Number, error)
// Mod returns the remainder of this number divided by another.
// Returns (nil, error) if modulo by zero or operation not supported.
Mod(other Number) (Number, error)
@@ -190,6 +193,42 @@ func (f *Float) Pow(other Number) (Number, error) {
return NewFloat(math.Pow(fF, otherF)), nil
}
+// PowInt returns this float raised to an integer power using binary exponentiation.
+// This is more efficient than repeated multiplication for large integer exponents.
+func (f *Float) PowInt(exp int) (Number, error) {
+ fF, err := f.Float64()
+ if err != nil {
+ return nil, fmt.Errorf("cannot power: %w", err)
+ }
+ return NewFloat(binaryExponentiationFloat(fF, exp)), nil
+}
+
+// binaryExponentiationFloat computes base^exp using the square-and-multiply algorithm.
+// Time Complexity: O(log exp)
+// Space Complexity: O(1)
+func binaryExponentiationFloat(base float64, exp int) float64 {
+ if exp == 0 {
+ return 1.0
+ }
+
+ // Handle negative exponents: base^-exp = 1 / (base^exp)
+ if exp < 0 {
+ return 1.0 / binaryExponentiationFloat(base, -exp)
+ }
+
+ res := 1.0
+ for exp > 0 {
+ // If exponent is odd, multiply result by current base
+ if exp%2 == 1 {
+ res *= base
+ }
+ // Square the base and divide exponent by 2
+ base *= base
+ exp /= 2
+ }
+ return res
+}
+
// Mod returns the remainder of this float divided by another.
func (f *Float) Mod(other Number) (Number, error) {
otherF, err := other.Float64()
@@ -384,6 +423,51 @@ func (r *Rat) Pow(other Number) (Number, error) {
return &Rat{n: result}, nil
}
+// PowInt returns this rational raised to an integer power using binary exponentiation.
+// This is more efficient than repeated multiplication for large integer exponents
+// and maintains exact rational arithmetic.
+func (r *Rat) PowInt(exp int) (Number, error) {
+ if exp == 0 {
+ return NewRat(1), nil
+ }
+
+ // Handle negative exponents: (a/b)^-exp = (b/a)^exp
+ if exp < 0 {
+ // Create reciprocal: b/a
+ reciprocal := &big.Rat{}
+ num := r.n.Num()
+ denom := r.n.Denom()
+ reciprocal.SetFrac(denom, num)
+ // Calculate reciprocal^(-exp)
+ return PowIntRat(reciprocal, -exp)
+ }
+
+ return PowIntRat(r.n, exp)
+}
+
+// PowIntRat computes rat^exp using binary exponentiation for *big.Rat.
+// This maintains exact rational arithmetic.
+func PowIntRat(rat *big.Rat, exp int) (Number, error) {
+ if exp == 0 {
+ return NewRat(1), nil
+ }
+
+ res := &big.Rat{}
+ res.SetInt64(1)
+ base := &big.Rat{}
+ base.Set(rat)
+
+ for exp > 0 {
+ if exp%2 == 1 {
+ res.Mul(res, base)
+ }
+ // Square the base: base = base * base
+ base.Mul(base, base)
+ exp /= 2
+ }
+ return &Rat{n: res}, nil
+}
+
// Mod returns the remainder of this rational divided by another.
func (r *Rat) Mod(other Number) (Number, error) {
if other.IsZero() {
@@ -476,6 +560,7 @@ func (s *StringNum) Sub(other Number) (Number, error) { return nil, fmt.Errorf("
func (s *StringNum) Mul(other Number) (Number, error) { return nil, fmt.Errorf("string not supported for multiplication") }
func (s *StringNum) Div(other Number) (Number, error) { return nil, fmt.Errorf("string not supported for division") }
func (s *StringNum) Pow(other Number) (Number, error) { return nil, fmt.Errorf("string not supported for power") }
+func (s *StringNum) PowInt(exp int) (Number, error) { return nil, fmt.Errorf("string not supported for integer power") }
func (s *StringNum) Mod(other Number) (Number, error) { return nil, fmt.Errorf("string not supported for modulo") }
func (s *StringNum) IsZero() bool { return false }
func (s *StringNum) IsNegative() bool { return false }
@@ -534,6 +619,9 @@ func (s *Symbol) Div(other Number) (Number, error) {
func (s *Symbol) Pow(other Number) (Number, error) {
return nil, fmt.Errorf("symbol not supported for power")
}
+func (s *Symbol) PowInt(exp int) (Number, error) {
+ return nil, fmt.Errorf("symbol not supported for integer power")
+}
func (s *Symbol) Mod(other Number) (Number, error) {
return nil, fmt.Errorf("symbol not supported for modulo")
}
diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go
index f17c6a4..1d693d1 100644
--- a/internal/rpn/operations.go
+++ b/internal/rpn/operations.go
@@ -116,6 +116,11 @@ type ConstantOperator interface {
ClearConstants()
}
+// PowerIntOperator defines the interface for integer power operations (**) using binary exponentiation.
+type PowerIntOperator interface {
+ FastPower(stack *Stack) error
+}
+
// Operator is the combined interface for all operator implementations.
// This allows RPN to depend on an abstraction instead of the concrete Operations type.
type Operator interface {
@@ -125,6 +130,7 @@ type Operator interface {
StackOperator
VariableOperator
ConstantOperator
+ PowerIntOperator
// SetMode sets the calculation mode for number formatting
SetMode(CalculationMode)
// AssignLeft assigns a value to a variable (for := operator)
@@ -197,6 +203,7 @@ func NewOperatorRegistry(op Operator) *OperatorRegistry {
registry.registerStandardOperator("*", func(stack *Stack) error { return op.Multiply(stack) })
registry.registerStandardOperator("/", func(stack *Stack) error { return op.Divide(stack) })
registry.registerStandardOperator("^", func(stack *Stack) error { return op.Power(stack) })
+ registry.registerStandardOperator("**", func(stack *Stack) error { return op.FastPower(stack) })
registry.registerStandardOperator("%", func(stack *Stack) error { return op.Modulo(stack) })
registry.registerStandardOperator("lg", func(stack *Stack) error { return op.Log2(stack) })
registry.registerStandardOperator("log", func(stack *Stack) error { return op.Log10(stack) })
@@ -421,6 +428,38 @@ func (o *Operations) Modulo(stack *Stack) error {
return nil
}
+// FastPower pops two values from stack, raises first to integer power of second (a ** b), and pushes result.
+// Uses binary exponentiation for efficiency with large integer exponents.
+func (o *Operations) FastPower(stack *Stack) error {
+ b, err := popStack(stack, "**")
+ if err != nil {
+ return err
+ }
+
+ a, err := popStack(stack, "**")
+ if err != nil {
+ return err
+ }
+
+ // Get the integer exponent from b
+ bVal, err := b.Float64()
+ if err != nil {
+ return buildError("**", fmt.Errorf("exponent must be a number: %w", err))
+ }
+
+ exp := int(bVal)
+ if float64(exp) != bVal {
+ return buildError("**", fmt.Errorf("exponent must be an integer, got %v", bVal))
+ }
+
+ result, err := a.PowInt(exp)
+ if err != nil {
+ return buildError("**", err)
+ }
+ stack.Push(result)
+ return nil
+}
+
// Log2 pops one value from stack, computes log base 2 (log₂(a)), and pushes result.
func (o *Operations) Log2(stack *Stack) error {
a, err := popStack(stack, "lg")
diff --git a/internal/rpn/operations_test.go b/internal/rpn/operations_test.go
index e27f318..6826bdd 100644
--- a/internal/rpn/operations_test.go
+++ b/internal/rpn/operations_test.go
@@ -341,6 +341,98 @@ func TestOperationsPowerNegativeExponent(t *testing.T) {
}
}
+func TestOperationsPowInt(t *testing.T) {
+ v := NewVariables()
+ o := NewOperations(v)
+ s := NewStack()
+
+ // Test PowInt(2, 10) = 1024
+ s.Push(NewNumber(2.0, FloatMode))
+ s.Push(NewNumber(10.0, FloatMode))
+
+ err := o.Power(s)
+ if err != nil {
+ t.Fatalf("Power(2^10) returned error: %v", err)
+ }
+
+ val, err := s.Pop()
+ if err != nil {
+ t.Fatalf("Pop() after Power(2^10) returned error: %v", err)
+ }
+ if v, err := val.Float64(); err != nil {
+ t.Fatalf("Float64() returned error: %v", err)
+ } else if v != 1024.0 {
+ t.Errorf("Power(2^10) = %v, want 1024.0", val)
+ }
+
+ // Test PowInt(2, -3) = 0.125
+ s.Push(NewNumber(2.0, FloatMode))
+ s.Push(NewNumber(-3.0, FloatMode))
+
+ err = o.Power(s)
+ if err != nil {
+ t.Fatalf("Power(2^-3) returned error: %v", err)
+ }
+
+ val, err = s.Pop()
+ if err != nil {
+ t.Fatalf("Pop() after Power(2^-3) returned error: %v", err)
+ }
+ if v, err := val.Float64(); err != nil {
+ t.Fatalf("Float64() returned error: %v", err)
+ } else if math.Abs(v-0.125) > 0.0001 {
+ t.Errorf("Power(2^-3) = %v, want 0.125", val)
+ }
+}
+
+func TestOperationsPowIntRat(t *testing.T) {
+ v := NewVariables()
+ o := NewOperations(v)
+ s := NewStack()
+
+ // Enable rational mode
+ o.SetMode(RationalMode)
+ defer o.SetMode(FloatMode)
+
+ // Test PowInt(2, 10) = 1024 in rational mode
+ s.Push(NewNumber(2.0, RationalMode))
+ s.Push(NewNumber(10.0, RationalMode))
+
+ err := o.Power(s)
+ if err != nil {
+ t.Fatalf("Power(2^10) in rational mode returned error: %v", err)
+ }
+
+ val, err := s.Pop()
+ if err != nil {
+ t.Fatalf("Pop() after Power(2^10) returned error: %v", err)
+ }
+ if v, err := val.Float64(); err != nil {
+ t.Fatalf("Float64() returned error: %v", err)
+ } else if v != 1024.0 {
+ t.Errorf("Power(2^10) in rational mode = %v, want 1024.0", val)
+ }
+
+ // Test PowInt(1/2, 3) = 1/8 = 0.125 in rational mode
+ s.Push(NewNumber(0.5, RationalMode))
+ s.Push(NewNumber(3.0, RationalMode))
+
+ err = o.Power(s)
+ if err != nil {
+ t.Fatalf("Power(0.5^3) in rational mode returned error: %v", err)
+ }
+
+ val, err = s.Pop()
+ if err != nil {
+ t.Fatalf("Pop() after Power(0.5^3) returned error: %v", err)
+ }
+ if v, err := val.Float64(); err != nil {
+ t.Fatalf("Float64() returned error: %v", err)
+ } else if math.Abs(v-0.125) > 0.0001 {
+ t.Errorf("Power(0.5^3) in rational mode = %v, want 0.125", val)
+ }
+}
+
func TestOperationsModulo(t *testing.T) {
v := NewVariables()
o := NewOperations(v)