diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-11 22:45:57 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-11 22:45:57 +0300 |
| commit | fe31559af818bcc1771b1a2eafc7f3aa1cac3f75 (patch) | |
| tree | 192545c30e748de5a8bc96bf7d116e5971ee15f7 | |
| parent | a1a55e2f1702b9f0f95ec10d3d9adcf35999f006 (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.md | 13 | ||||
| -rw-r--r-- | integrationtests/cli_test.go | 95 | ||||
| -rw-r--r-- | internal/rpn/number.go | 88 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 39 | ||||
| -rw-r--r-- | internal/rpn/operations_test.go | 92 |
5 files changed, 326 insertions, 1 deletions
@@ -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) |
