summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-11 20:53:58 +0300
committerPaul Buetow <paul@buetow.org>2026-04-11 20:53:58 +0300
commit4465f7abdc72887a422b2ddd9afc43ee811b2e9b (patch)
tree98182a806c34607810c97d832b1d0b3f2766267b /internal
parentead2412b0f0d23b2cfc3b265a3dae8841f8c84f7 (diff)
Refactor number.go to return errors instead of panicking
- Changed Number interface methods to return errors instead of panicking - Float64() now returns (float64, error) - Add, Sub, Mul, Pow, Compare, Bool() now return (Number, error) or (int, error) - StringNum and Symbol now return errors for unsupported operations - Added IsString() and IsSymbol() to Number interface - Removed unused arithmetic.go file - Updated operations.go, boolean_ops.go, hyper.go to handle errors - Added constants registry (internal/rpn/constants.go) with built-in math constants - Added constants_test.go with comprehensive unit tests - Updated README.md with constants documentation
Diffstat (limited to 'internal')
-rw-r--r--internal/rpn/arithmetic.go186
-rw-r--r--internal/rpn/boolean_ops.go66
-rw-r--r--internal/rpn/constants.go170
-rw-r--r--internal/rpn/constants_test.go356
-rw-r--r--internal/rpn/hyper.go73
-rw-r--r--internal/rpn/number.go251
-rw-r--r--internal/rpn/operations.go247
-rw-r--r--internal/rpn/rpn_state.go9
-rw-r--r--internal/rpn/variable.go6
9 files changed, 1035 insertions, 329 deletions
diff --git a/internal/rpn/arithmetic.go b/internal/rpn/arithmetic.go
deleted file mode 100644
index 049cf25..0000000
--- a/internal/rpn/arithmetic.go
+++ /dev/null
@@ -1,186 +0,0 @@
-// 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
index d07aa26..21501a9 100644
--- a/internal/rpn/boolean_ops.go
+++ b/internal/rpn/boolean_ops.go
@@ -28,7 +28,16 @@ func (o *BooleanOperations) GT(stack *Stack) error {
return fmt.Errorf("insufficient operands for gt: %w", err)
}
- stack.Push(NewFloatFromBool(a.Float64() > b.Float64()))
+ aVal, err := a.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for a: %w", err)
+ }
+ bVal, err := b.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for b: %w", err)
+ }
+
+ stack.Push(NewFloatFromBool(aVal > bVal))
return nil
}
@@ -44,7 +53,16 @@ func (o *BooleanOperations) LT(stack *Stack) error {
return fmt.Errorf("insufficient operands for lt: %w", err)
}
- stack.Push(NewFloatFromBool(a.Float64() < b.Float64()))
+ aVal, err := a.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for a: %w", err)
+ }
+ bVal, err := b.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for b: %w", err)
+ }
+
+ stack.Push(NewFloatFromBool(aVal < bVal))
return nil
}
@@ -60,7 +78,16 @@ func (o *BooleanOperations) GTE(stack *Stack) error {
return fmt.Errorf("insufficient operands for gte: %w", err)
}
- stack.Push(NewFloatFromBool(a.Float64() >= b.Float64()))
+ aVal, err := a.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for a: %w", err)
+ }
+ bVal, err := b.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for b: %w", err)
+ }
+
+ stack.Push(NewFloatFromBool(aVal >= bVal))
return nil
}
@@ -76,7 +103,16 @@ func (o *BooleanOperations) LTE(stack *Stack) error {
return fmt.Errorf("insufficient operands for lte: %w", err)
}
- stack.Push(NewFloatFromBool(a.Float64() <= b.Float64()))
+ aVal, err := a.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for a: %w", err)
+ }
+ bVal, err := b.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for b: %w", err)
+ }
+
+ stack.Push(NewFloatFromBool(aVal <= bVal))
return nil
}
@@ -92,7 +128,16 @@ func (o *BooleanOperations) EQ(stack *Stack) error {
return fmt.Errorf("insufficient operands for eq: %w", err)
}
- stack.Push(NewFloatFromBool(a.Float64() == b.Float64()))
+ aVal, err := a.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for a: %w", err)
+ }
+ bVal, err := b.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for b: %w", err)
+ }
+
+ stack.Push(NewFloatFromBool(aVal == bVal))
return nil
}
@@ -108,6 +153,15 @@ func (o *BooleanOperations) NEQ(stack *Stack) error {
return fmt.Errorf("insufficient operands for neq: %w", err)
}
- stack.Push(NewFloatFromBool(a.Float64() != b.Float64()))
+ aVal, err := a.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for a: %w", err)
+ }
+ bVal, err := b.Float64()
+ if err != nil {
+ return fmt.Errorf("failed to get float64 value for b: %w", err)
+ }
+
+ stack.Push(NewFloatFromBool(aVal != bVal))
return nil
}
diff --git a/internal/rpn/constants.go b/internal/rpn/constants.go
new file mode 100644
index 0000000..53d9df6
--- /dev/null
+++ b/internal/rpn/constants.go
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package rpn
+
+import (
+ "math"
+ "sort"
+ "sync"
+)
+
+// ConstantsProvider defines the interface for reading constant values.
+type ConstantsProvider interface {
+ GetConstant(name string) (float64, bool)
+ ListConstants() []ConstantInfo
+ Count() int
+ HasConstant(name string) bool
+ SetConstant(name string, value float64) error
+ ClearConstants()
+ ReloadBuiltInConstants()
+}
+
+// ConstantInfo represents a single constant with its name and value.
+type ConstantInfo struct {
+ Name string
+ Value float64
+}
+
+// Constants stores constant name-value pairs for RPN calculations.
+// It provides thread-safe access to constant storage.
+type Constants struct {
+ mu sync.RWMutex
+ constants map[string]float64
+}
+
+// NewConstants creates and initializes a new Constants instance with built-in constants.
+func NewConstants() *Constants {
+ c := &Constants{
+ constants: make(map[string]float64),
+ }
+ c.loadBuiltInConstants()
+ return c
+}
+
+// loadBuiltInConstants loads the standard mathematical constants.
+func (c *Constants) loadBuiltInConstants() {
+ // Pi (π) - ratio of a circle's circumference to its diameter
+ c.constants["pi"] = math.Pi
+ c.constants["π"] = math.Pi
+
+ // Euler's number (e) - base of natural logarithm
+ c.constants["e"] = math.E
+ c.constants["euler"] = math.E
+
+ // Golden ratio (φ)
+ c.constants["phi"] = 1.618033988749895
+ c.constants["φ"] = 1.618033988749895
+
+ // Square root of 2
+ c.constants["sqrt2"] = 1.414213562373095
+ c.constants["√2"] = 1.414213562373095
+
+ // Infinity
+ c.constants["inf"] = math.Inf(1)
+ c.constants["infinity"] = math.Inf(1)
+
+ // NaN (Not a Number)
+ c.constants["nan"] = math.NaN()
+}
+
+// SetConstant assigns a value to a constant name.
+func (c *Constants) SetConstant(name string, value float64) error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.constants[name] = value
+ return nil
+}
+
+// GetConstant retrieves the value of a constant.
+// Returns the value and true if found, or 0 and false if not found.
+func (c *Constants) GetConstant(name string) (float64, bool) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ value, exists := c.constants[name]
+ return value, exists
+}
+
+// ListConstants returns a sorted list of all constant names and their values.
+func (c *Constants) ListConstants() []ConstantInfo {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ var infos []ConstantInfo
+ for name, value := range c.constants {
+ infos = append(infos, ConstantInfo{Name: name, Value: value})
+ }
+
+ // Sort by name for consistent output
+ sort.Slice(infos, func(i, j int) bool {
+ return infos[i].Name < infos[j].Name
+ })
+
+ return infos
+}
+
+// ClearConstants removes all constants from storage.
+// Note: This clears only user-defined constants; built-in constants are preserved.
+func (c *Constants) ClearConstants() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ // Remove only user-defined constants (not built-in ones)
+ builtIns := map[string]bool{
+ "pi": true, "π": true,
+ "e": true, "euler": true,
+ "phi": true, "φ": true,
+ "sqrt2": true, "√2": true,
+ "inf": true, "infinity": true,
+ "nan": true,
+ }
+ for k := range c.constants {
+ if !builtIns[k] {
+ delete(c.constants, k)
+ }
+ }
+}
+
+// ReloadBuiltInConstants restores all built-in constants.
+// This is called internally when ClearConstants is used to ensure
+// built-in constants are preserved.
+func (c *Constants) ReloadBuiltInConstants() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ // First remove only user-defined constants
+ builtIns := map[string]bool{
+ "pi": true, "π": true,
+ "e": true, "euler": true,
+ "phi": true, "φ": true,
+ "sqrt2": true, "√2": true,
+ "inf": true, "infinity": true,
+ "nan": true,
+ }
+ for k := range c.constants {
+ if !builtIns[k] {
+ delete(c.constants, k)
+ }
+ }
+ // Then reload built-in constants
+ c.loadBuiltInConstants()
+}
+
+// Count returns the number of defined constants.
+func (c *Constants) Count() int {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ return len(c.constants)
+}
+
+// HasConstant checks if a constant exists.
+func (c *Constants) HasConstant(name string) bool {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ _, exists := c.constants[name]
+ return exists
+}
diff --git a/internal/rpn/constants_test.go b/internal/rpn/constants_test.go
new file mode 100644
index 0000000..3a17b76
--- /dev/null
+++ b/internal/rpn/constants_test.go
@@ -0,0 +1,356 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package rpn
+
+import (
+ "math"
+ "strings"
+ "testing"
+)
+
+func TestNewConstants(t *testing.T) {
+ c := NewConstants()
+ if c == nil {
+ t.Fatal("NewConstants() returned nil")
+ }
+ if c.Count() == 0 {
+ t.Error("NewConstants() should have built-in constants")
+ }
+}
+
+func TestConstants_GetConstant(t *testing.T) {
+ c := NewConstants()
+
+ tests := []struct {
+ name string
+ key string
+ expected float64
+ }{
+ {"pi", "pi", math.Pi},
+ {"Pi with Greek letter", "π", math.Pi},
+ {"Euler's number", "e", math.E},
+ {"Euler with name", "euler", math.E},
+ {"Golden ratio", "phi", 1.618033988749895},
+ {"Golden ratio with Greek letter", "φ", 1.618033988749895},
+ {"Square root of 2", "sqrt2", 1.414213562373095},
+ {"Square root of 2 with symbol", "√2", 1.414213562373095},
+ {"Infinity", "inf", math.Inf(1)},
+ {"Infinity with name", "infinity", math.Inf(1)},
+ {"NaN", "nan", math.NaN()},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ val, exists := c.GetConstant(tt.key)
+ if !exists {
+ t.Errorf("Constant %q should exist", tt.key)
+ return
+ }
+ if tt.key == "nan" {
+ if !math.IsNaN(val) {
+ t.Errorf("Constant %q = %v, want NaN", tt.key, val)
+ }
+ } else if tt.key == "inf" || tt.key == "infinity" {
+ if val != math.Inf(1) {
+ t.Errorf("Constant %q = %v, want %v", tt.key, val, math.Inf(1))
+ }
+ } else {
+ if val != tt.expected {
+ t.Errorf("Constant %q = %v, want %v", tt.key, val, tt.expected)
+ }
+ }
+ })
+ }
+}
+
+func TestConstants_GetConstant_NonExistent(t *testing.T) {
+ c := NewConstants()
+
+ val, exists := c.GetConstant("nonexistent")
+ if exists {
+ t.Errorf("Non-existent constant should return exists=false")
+ }
+ if val != 0 {
+ t.Errorf("Non-existent constant value should be 0, got %v", val)
+ }
+}
+
+func TestConstants_Count(t *testing.T) {
+ c := NewConstants()
+ count := c.Count()
+
+ // Should have at least: pi, e, phi, sqrt2, inf, nan = 6
+ if count < 6 {
+ t.Errorf("Count = %d, want at least 6 built-in constants", count)
+ }
+}
+
+func TestConstants_HasConstant(t *testing.T) {
+ c := NewConstants()
+
+ if !c.HasConstant("pi") {
+ t.Error("HasConstant(\"pi\") should return true")
+ }
+ if !c.HasConstant("e") {
+ t.Error("HasConstant(\"e\") should return true")
+ }
+ if c.HasConstant("nonexistent") {
+ t.Error("HasConstant(\"nonexistent\") should return false")
+ }
+}
+
+func TestConstants_ListConstants(t *testing.T) {
+ c := NewConstants()
+ infos := c.ListConstants()
+
+ if len(infos) == 0 {
+ t.Error("ListConstants() should return at least one constant")
+ }
+
+ // Check that pi is in the list
+ foundPi := false
+ foundE := false
+ for _, info := range infos {
+ if info.Name == "pi" {
+ foundPi = true
+ if info.Value != math.Pi {
+ t.Errorf("pi constant value = %v, want %v", info.Value, math.Pi)
+ }
+ }
+ if info.Name == "e" {
+ foundE = true
+ if info.Value != math.E {
+ t.Errorf("e constant value = %v, want %v", info.Value, math.E)
+ }
+ }
+ }
+
+ if !foundPi {
+ t.Error("pi constant not found in ListConstants() output")
+ }
+ if !foundE {
+ t.Error("e constant not found in ListConstants() output")
+ }
+}
+
+func TestConstants_ListConstantsSorted(t *testing.T) {
+ c := NewConstants()
+ infos := c.ListConstants()
+
+ // Verify the list is sorted alphabetically by name
+ for i := 0; i < len(infos)-1; i++ {
+ if infos[i].Name > infos[i+1].Name {
+ t.Errorf("ListConstants() not sorted: %q > %q", infos[i].Name, infos[i+1].Name)
+ }
+ }
+}
+
+func TestConstants_SetConstant(t *testing.T) {
+ c := NewConstants()
+
+ // Test setting a custom constant
+ err := c.SetConstant("custom", 42.0)
+ if err != nil {
+ t.Errorf("SetConstant() returned error: %v", err)
+ }
+
+ val, exists := c.GetConstant("custom")
+ if !exists {
+ t.Error("Custom constant should exist after SetConstant()")
+ }
+ if val != 42.0 {
+ t.Errorf("Custom constant value = %v, want 42.0", val)
+ }
+}
+
+func TestConstants_ClearConstants(t *testing.T) {
+ c := NewConstants()
+
+ // First add a user-defined constant
+ c.SetConstant("custom", 42.0)
+
+ // Clear the constants
+ c.ClearConstants()
+
+ // Built-in constants should still exist (pi, e, phi, sqrt2, inf, nan = 6 + 1 duplicate each = 11)
+ // But custom should be removed
+ count := c.Count()
+ if count < 11 {
+ t.Errorf("Count after ClearConstants() = %d, want at least 11 (built-in constants)", count)
+ }
+
+ // Verify custom constant is gone
+ _, exists := c.GetConstant("custom")
+ if exists {
+ t.Error("Custom constant should be removed after ClearConstants()")
+ }
+}
+
+func TestConstants_ThreadSafety(t *testing.T) {
+ c := NewConstants()
+
+ done := make(chan bool, 10)
+ for i := 0; i < 10; i++ {
+ go func(id int) {
+ name := "thread" + string(rune(id+'0'))
+ c.SetConstant(name, float64(id*10))
+ val, exists := c.GetConstant(name)
+ if !exists {
+ t.Errorf("Thread %d: constant %q should exist", id, name)
+ }
+ if val != float64(id*10) {
+ t.Errorf("Thread %d: constant %q = %v, want %v", id, name, val, float64(id*10))
+ }
+ done <- true
+ }(i)
+ }
+
+ for i := 0; i < 10; i++ {
+ <-done
+ }
+}
+
+func TestConstants_RetrieveInRPN(t *testing.T) {
+ v := NewVariables()
+ r := NewRPN(v)
+
+ // Test pi constant
+ result, err := r.ParseAndEvaluate("pi")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"pi\") returned error: %v", err)
+ }
+ if result != "3.141592654" {
+ t.Errorf("ParseAndEvaluate(\"pi\") = %q, want \"3.141592654\"", result)
+ }
+
+ // Create a new RPN instance for each test to avoid stack state conflicts
+ v2 := NewVariables()
+ r2 := NewRPN(v2)
+
+ // Test e constant
+ result, err = r2.ParseAndEvaluate("e")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"e\") returned error: %v", err)
+ }
+ if result != "2.718281828" {
+ t.Errorf("ParseAndEvaluate(\"e\") = %q, want \"2.718281828\"", result)
+ }
+
+ // Create a new RPN instance for the next test
+ v3 := NewVariables()
+ r3 := NewRPN(v3)
+
+ // Test pi in expression
+ result, err = r3.ParseAndEvaluate("pi 2 *")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"pi 2 *\") returned error: %v", err)
+ }
+ if result != "6.283185307" {
+ t.Errorf("ParseAndEvaluate(\"pi 2 *\") = %q, want \"6.283185307\"", result)
+ }
+
+ // Create a new RPN instance for phi test
+ v4 := NewVariables()
+ r4 := NewRPN(v4)
+
+ // Test phi constant
+ result, err = r4.ParseAndEvaluate("phi")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"phi\") returned error: %v", err)
+ }
+ if result != "1.618033989" {
+ t.Errorf("ParseAndEvaluate(\"phi\") = %q, want \"1.618033989\"", result)
+ }
+}
+
+func TestConstants_RetrieveWithGreekLetters(t *testing.T) {
+ v := NewVariables()
+ r := NewRPN(v)
+
+ // Test pi with Greek letter
+ result, err := r.ParseAndEvaluate("π")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"π\") returned error: %v", err)
+ }
+ if result != "3.141592654" {
+ t.Errorf("ParseAndEvaluate(\"π\") = %q, want \"3.141592654\"", result)
+ }
+
+ // Create a new RPN instance for phi test
+ v2 := NewVariables()
+ r2 := NewRPN(v2)
+
+ // Test phi with Greek letter
+ result, err = r2.ParseAndEvaluate("φ")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"φ\") returned error: %v", err)
+ }
+ if result != "1.618033989" {
+ t.Errorf("ParseAndEvaluate(\"φ\") = %q, want \"1.618033989\"", result)
+ }
+}
+
+func TestConstants_ConflictWithVariables(t *testing.T) {
+ v := NewVariables()
+ r := NewRPN(v)
+
+ // First set a variable named pi
+ result, err := r.ParseAndEvaluate("pi = 3.0")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"pi = 3.0\") returned error: %v", err)
+ }
+ if result != "pi = 3" {
+ t.Errorf("ParseAndEvaluate(\"pi = 3.0\") = %q, want \"pi = 3\"", result)
+ }
+
+ // Now using pi should get the variable value, not the constant
+ result, err = r.ParseAndEvaluate("pi")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"pi\") after variable set returned error: %v", err)
+ }
+ if result != "3" {
+ t.Errorf("ParseAndEvaluate(\"pi\") after variable set = %q, want \"3\"", result)
+ }
+}
+
+func TestConstantsCommand(t *testing.T) {
+ v := NewVariables()
+ r := NewRPN(v)
+ result, err := r.ParseAndEvaluate("constants")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"constants\") returned error: %v", err)
+ }
+ if !strings.Contains(result, "pi") || !strings.Contains(result, "e") {
+ t.Errorf("constants output should contain pi and e, got: %s", result)
+ }
+}
+
+func TestClearConstantsCommand(t *testing.T) {
+ v := NewVariables()
+ r := NewRPN(v)
+
+ // First add a custom constant
+ result, err := r.ParseAndEvaluate("custom 42 =")
+ if err != nil {
+ t.Fatalf("Failed to set custom constant: %v", err)
+ }
+
+ // Clear constants
+ result, err = r.ParseAndEvaluate("clearconstants")
+ if err != nil {
+ t.Fatalf("ParseAndEvaluate(\"clearconstants\") returned error: %v", err)
+ }
+ if result != "All constants cleared" {
+ t.Errorf("clearconstants result = %q, want \"All constants cleared\"", result)
+ }
+
+ // Built-in constants should still exist
+ result, err = r.ParseAndEvaluate("pi")
+ if err != nil {
+ t.Fatalf("pi constant should still work after clearconstants: %v", err)
+ }
+ if result != "3.141592654" {
+ t.Errorf("pi after clearconstants = %q, want \"3.141592654\"", result)
+ }
+}
diff --git a/internal/rpn/hyper.go b/internal/rpn/hyper.go
index 58a0ca2..af589ca 100644
--- a/internal/rpn/hyper.go
+++ b/internal/rpn/hyper.go
@@ -42,7 +42,11 @@ func (o *HyperOperations) HyperAdd(stack *Stack) error {
// Process left-associative with Number interface
sum := 0.0
for i := 0; i < len(values); i++ {
- sum += values[i].Float64()
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperadd: failed to get float64 value: %w", err)
+ }
+ sum += val
}
stack.Push(NewNumber(sum, o.mode))
return nil
@@ -60,7 +64,11 @@ func (o *HyperOperations) HyperMultiply(stack *Stack) error {
if err != nil {
return fmt.Errorf("hypermultiply: %w", err)
}
- product *= val.Float64()
+ floatVal, err := val.Float64()
+ if err != nil {
+ return fmt.Errorf("hypermultiply: failed to get float64 value: %w", err)
+ }
+ product *= floatVal
}
stack.Push(NewNumber(product, o.mode))
return nil
@@ -88,9 +96,17 @@ func (o *HyperOperations) HyperSubtract(stack *Stack) error {
}
// Process left-associative with Number interface
- result := values[0].Float64()
+ firstVal, err := values[0].Float64()
+ if err != nil {
+ return fmt.Errorf("hypersubtract: failed to get float64 value: %w", err)
+ }
+ result := firstVal
for i := 1; i < len(values); i++ {
- result -= values[i].Float64()
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hypersubtract: failed to get float64 value: %w", err)
+ }
+ result -= val
}
stack.Push(NewNumber(result, o.mode))
return nil
@@ -118,9 +134,16 @@ func (o *HyperOperations) HyperDivide(stack *Stack) error {
}
// Process left-associative with Number interface
- result := values[0].Float64()
+ firstVal, err := values[0].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperdivide: failed to get float64 value: %w", err)
+ }
+ result := firstVal
for i := 1; i < len(values); i++ {
- val := values[i].Float64()
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperdivide: failed to get float64 value: %w", err)
+ }
if val == 0 {
return fmt.Errorf("division by zero")
}
@@ -152,9 +175,17 @@ func (o *HyperOperations) HyperPower(stack *Stack) error {
}
// Process left-associative with Number interface
- result := values[0].Float64()
+ firstVal, err := values[0].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperpower: failed to get float64 value: %w", err)
+ }
+ result := firstVal
for i := 1; i < len(values); i++ {
- result = math.Pow(result, values[i].Float64())
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperpower: failed to get float64 value: %w", err)
+ }
+ result = math.Pow(result, val)
}
stack.Push(NewNumber(result, o.mode))
return nil
@@ -182,9 +213,16 @@ func (o *HyperOperations) HyperModulo(stack *Stack) error {
}
// Process left-associative with Number interface
- result := values[0].Float64()
+ firstVal, err := values[0].Float64()
+ if err != nil {
+ return fmt.Errorf("hypermodulo: failed to get float64 value: %w", err)
+ }
+ result := firstVal
for i := 1; i < len(values); i++ {
- val := values[i].Float64()
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hypermodulo: failed to get float64 value: %w", err)
+ }
if val == 0 {
return fmt.Errorf("modulo by zero")
}
@@ -219,7 +257,10 @@ func (o *HyperOperations) HyperLog2(stack *Stack) error {
// Sum the log2 of all values with Number interface
var result float64 = 0
for i := 0; i < len(values); i++ {
- val := values[i].Float64()
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperlog2: failed to get float64 value: %w", err)
+ }
if val <= 0 {
return fmt.Errorf("hyperlog2 undefined for non-positive numbers")
}
@@ -256,7 +297,10 @@ func (o *HyperOperations) HyperLog10(stack *Stack) error {
// Sum the log10 of all values
var result float64 = 0
for i := 0; i < len(values); i++ {
- val := values[i].Float64()
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperlog10: failed to get float64 value: %w", err)
+ }
if val <= 0 {
return fmt.Errorf("hyperlog10 undefined for non-positive numbers")
}
@@ -293,7 +337,10 @@ func (o *HyperOperations) HyperLn(stack *Stack) error {
// 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()
+ val, err := values[i].Float64()
+ if err != nil {
+ return fmt.Errorf("hyperln: failed to get float64 value: %w", err)
+ }
if val <= 0 {
return fmt.Errorf("hyperln undefined for non-positive numbers")
}
diff --git a/internal/rpn/number.go b/internal/rpn/number.go
index dc2b2cb..bb6f765 100644
--- a/internal/rpn/number.go
+++ b/internal/rpn/number.go
@@ -15,32 +15,43 @@ import (
type Number interface {
// String returns the string representation of the number.
String() string
- // Float64 returns the float64 representation, or panics if not representable.
- Float64() float64
+ // Float64 returns the float64 representation.
+ // Returns error if the number is not representable (e.g., StringNum, Symbol).
+ Float64() (float64, error)
// Add returns the sum of this number and another.
- Add(other Number) Number
+ // Returns error if the operation is not supported (e.g., StringNum, Symbol).
+ Add(other Number) (Number, error)
// Sub returns the difference of this number and another.
- Sub(other Number) Number
+ // Returns error if the operation is not supported (e.g., StringNum, Symbol).
+ Sub(other Number) (Number, error)
// Mul returns the product of this number and another.
- Mul(other Number) Number
+ // Returns error if the operation is not supported (e.g., StringNum, Symbol).
+ Mul(other Number) (Number, error)
// Div returns the quotient of this number and another.
- // Returns (nil, error) if division by zero.
+ // Returns (nil, error) if division by zero or operation not supported.
Div(other Number) (Number, error)
// Pow returns this number raised to the power of another.
- Pow(other Number) Number
+ // Returns error if the operation is not supported (e.g., StringNum, Symbol).
+ Pow(other Number) (Number, error)
// Mod returns the remainder of this number divided by another.
- // Returns (nil, error) if modulo by zero.
+ // Returns (nil, error) if modulo by zero or operation not supported.
Mod(other Number) (Number, error)
// IsZero returns true if the number is zero.
IsZero() bool
// IsNegative returns true if the number is negative.
IsNegative() bool
// Compare returns -1, 0, or 1 if this number is less than, equal to, or greater than another.
- Compare(other Number) int
+ // Returns error if the operation is not supported (e.g., StringNum, Symbol).
+ Compare(other Number) (int, error)
// IsBool returns true if this number represents a boolean value.
IsBool() bool
- // Bool returns the boolean value, or false if not a boolean.
- Bool() bool
+ // Bool returns the boolean value.
+ // Returns error if the number is not a boolean.
+ Bool() (bool, error)
+ // IsString returns true if this number represents a string value.
+ IsString() bool
+ // IsSymbol returns true if this number represents a symbol.
+ IsSymbol() bool
}
// NewNumber creates a Number from a float64 value.
@@ -82,14 +93,14 @@ func (f *Float) String() string {
}
// Float64 returns the float64 value.
-func (f *Float) Float64() float64 {
+func (f *Float) Float64() (float64, error) {
if f.isBool {
if f.boolVal {
- return 1
+ return 1, nil
}
- return 0
+ return 0, nil
}
- return f.n
+ return f.n, nil
}
// IsBool returns true if this number represents a boolean value.
@@ -97,51 +108,79 @@ func (f *Float) IsBool() bool {
return f.isBool
}
-// Bool returns the boolean value, or false if not a boolean.
-func (f *Float) Bool() bool {
- return f.boolVal
+// Bool returns the boolean value.
+// Returns error if the number is not a boolean.
+func (f *Float) Bool() (bool, error) {
+ if !f.isBool {
+ return false, fmt.Errorf("not a boolean")
+ }
+ return f.boolVal, nil
}
// Add returns the sum of two float numbers.
-func (f *Float) Add(other Number) Number {
+func (f *Float) Add(other Number) (Number, error) {
+ otherF, err := other.Float64()
+ if err != nil {
+ return nil, fmt.Errorf("cannot add: %w", err)
+ }
// Use Float64() to handle both regular numbers and boolean values
- return NewFloat(f.Float64() + other.Float64())
+ return NewFloat(f.n + otherF), nil
}
// Sub returns the difference of two float numbers.
-func (f *Float) Sub(other Number) Number {
+func (f *Float) Sub(other Number) (Number, error) {
+ otherF, err := other.Float64()
+ if err != nil {</