diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-11 20:53:58 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-11 20:53:58 +0300 |
| commit | 4465f7abdc72887a422b2ddd9afc43ee811b2e9b (patch) | |
| tree | 98182a806c34607810c97d832b1d0b3f2766267b /internal | |
| parent | ead2412b0f0d23b2cfc3b265a3dae8841f8c84f7 (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.go | 186 | ||||
| -rw-r--r-- | internal/rpn/boolean_ops.go | 66 | ||||
| -rw-r--r-- | internal/rpn/constants.go | 170 | ||||
| -rw-r--r-- | internal/rpn/constants_test.go | 356 | ||||
| -rw-r--r-- | internal/rpn/hyper.go | 73 | ||||
| -rw-r--r-- | internal/rpn/number.go | 251 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 247 | ||||
| -rw-r--r-- | internal/rpn/rpn_state.go | 9 | ||||
| -rw-r--r-- | internal/rpn/variable.go | 6 |
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 { |
