diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-25 16:58:30 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-25 16:58:30 +0200 |
| commit | 5cb2c02eaffddab3ace1bf500e95fac5ea219f05 (patch) | |
| tree | ca143c5f3edceec1c235fd83a9c1d05045175b6b | |
| parent | 8cb5305609175e724a1450f787b562ad3fac638a (diff) | |
feat: Implement boolean operators and mixed boolean-numeric arithmetic
- Added boolean operators: GT, LT, GTE, LTE, EQ, NEQ
- Registered as: gt, lt, gte, lte, eq, neq
- Also registered symbols: >, <, >=, <=, ==, !=
- Added boolean literal support:
- true and false are now recognized as boolean values
- Can be used in expressions like: true 2 *, 0 false +
- Fixed boolean formatting:
- Show command now displays booleans as 'true'/'false'
- Single result output uses Value.String() instead of toNumber()
- Created boolean_test.go with comprehensive tests:
- TestBooleanOperators: Tests all 6 boolean operators
- TestBooleanToNumberCoercion: Tests automatic coercion in arithmetic
- TestMixedBooleanNumericArithmetic: Tests mixed boolean-numeric arithmetic
- TestBooleanShowFormat: Tests Show command displays booleans correctly
All tests pass and the binary builds correctly.
| -rwxr-xr-x | gt | bin | 0 -> 3705610 bytes | |||
| -rw-r--r-- | internal/rpn/boolean_test.go | 281 | ||||
| -rw-r--r-- | internal/rpn/operations.go | 120 | ||||
| -rw-r--r-- | internal/rpn/rpn.go | 14 |
4 files changed, 413 insertions, 2 deletions
| Binary files differ diff --git a/internal/rpn/boolean_test.go b/internal/rpn/boolean_test.go new file mode 100644 index 0000000..f748cc9 --- /dev/null +++ b/internal/rpn/boolean_test.go @@ -0,0 +1,281 @@ +package rpn + +import ( + "testing" +) + +// TestBooleanOperators tests that boolean comparison operators work correctly. +func TestBooleanOperators(t *testing.T) { + tests := []struct { + name string + expression string + expected string + description string + }{ + { + name: "gt true case", + expression: "5 3 gt", + expected: "true", + description: "5 > 3 = true", + }, + { + name: "gt false case", + expression: "3 5 gt", + expected: "false", + description: "3 > 5 = false", + }, + { + name: "gt equal case", + expression: "5 5 gt", + expected: "false", + description: "5 > 5 = false", + }, + { + name: "lt true case", + expression: "3 5 lt", + expected: "true", + description: "3 < 5 = true", + }, + { + name: "lt false case", + expression: "5 3 lt", + expected: "false", + description: "5 < 3 = false", + }, + { + name: "gte true case", + expression: "5 5 gte", + expected: "true", + description: "5 >= 5 = true", + }, + { + name: "gte false case", + expression: "3 5 gte", + expected: "false", + description: "3 >= 5 = false", + }, + { + name: "lte true case", + expression: "5 5 lte", + expected: "true", + description: "5 <= 5 = true", + }, + { + name: "lte false case", + expression: "5 3 lte", + expected: "false", + description: "5 <= 3 = false", + }, + { + name: "eq true case", + expression: "5 5 eq", + expected: "true", + description: "5 == 5 = true", + }, + { + name: "eq false case", + expression: "5 3 eq", + expected: "false", + description: "5 == 3 = false", + }, + { + name: "neq true case", + expression: "5 3 neq", + expected: "true", + description: "5 != 3 = true", + }, + { + name: "neq false case", + expression: "5 5 neq", + expected: "false", + description: "5 != 5 = false", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vars := NewVariables() + rpnCalc := NewRPN(vars) + + result, err := rpnCalc.ParseAndEvaluate(tt.expression) + + if err != nil { + t.Fatalf("Evaluate(%q) returned error: %v", tt.expression, err) + } + + if result != tt.expected { + t.Errorf("Evaluate(%q) = %q, want %q (%s)", tt.expression, result, tt.expected, tt.description) + } + }) + } +} + +// TestBooleanToNumberCoercion tests that boolean values are automatically +// coerced to numbers (true → 1, false → 0) in arithmetic operations. +func TestBooleanToNumberCoercion(t *testing.T) { + tests := []struct { + name string + expression string + expected string + description string + }{ + { + name: "true in addition", + expression: "true 5 +", + expected: "6", + description: "true (1) + 5 = 6", + }, + { + name: "false in addition", + expression: "false 5 +", + expected: "5", + description: "false (0) + 5 = 5", + }, + { + name: "true in multiplication", + expression: "true 5 *", + expected: "5", + description: "true (1) * 5 = 5", + }, + { + name: "false in multiplication", + expression: "false 5 *", + expected: "0", + description: "false (0) * 5 = 0", + }, + { + name: "false in subtraction", + expression: "5 false -", + expected: "5", + description: "5 - false (0) = 5", + }, + { + name: "mixed boolean-numeric", + expression: "true false +", + expected: "1", + description: "true (1) + false (0) = 1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vars := NewVariables() + rpnCalc := NewRPN(vars) + + result, err := rpnCalc.ParseAndEvaluate(tt.expression) + + if err != nil { + t.Fatalf("Evaluate(%q) returned error: %v", tt.expression, err) + } + + if result != tt.expected { + t.Errorf("Evaluate(%q) = %q, want %q (%s)", tt.expression, result, tt.expected, tt.description) + } + }) + } +} + +// TestMixedBooleanNumericArithmetic tests mixed boolean-numeric arithmetic. +func TestMixedBooleanNumericArithmetic(t *testing.T) { + tests := []struct { + name string + expression string + expected string + description string + }{ + { + name: "5 3 gt 1 +", + expression: "5 3 gt 1 +", + expected: "2", + description: "5 > 3 is true (1), 1 + 1 = 2", + }, + { + name: "3 5 gt 1 +", + expression: "3 5 gt 1 +", + expected: "1", + description: "3 > 5 is false (0), 0 + 1 = 1", + }, + { + name: "true 2 *", + expression: "true 2 *", + expected: "2", + description: "true (1) * 2 = 2", + }, + { + name: "0 false +", + expression: "0 false +", + expected: "0", + description: "0 + false (0) = 0", + }, + { + name: "9 3 gt 4 5 lt +", + expression: "9 3 gt 4 5 lt +", + expected: "2", + description: "9 > 3 is true (1), 4 < 5 is true (1), 1 + 1 = 2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vars := NewVariables() + rpnCalc := NewRPN(vars) + + result, err := rpnCalc.ParseAndEvaluate(tt.expression) + + if err != nil { + t.Fatalf("Evaluate(%q) returned error: %v", tt.expression, err) + } + + if result != tt.expected { + t.Errorf("Evaluate(%q) = %q, want %q (%s)", tt.expression, result, tt.expected, tt.description) + } + }) + } +} + +// TestBooleanShowFormat tests that Show command displays boolean values as true/false +func TestBooleanShowFormat(t *testing.T) { + tests := []struct { + name string + expression string + expected string + }{ + { + name: "show true", + expression: "true show", + expected: "true", + }, + { + name: "show false", + expression: "false show", + expected: "false", + }, + { + name: "show mixed stack", + expression: "1 true 2 show", + expected: "1 true 2", + }, + { + name: "show comparison result", + expression: "5 3 gt show", + expected: "true", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vars := NewVariables() + rpnCalc := NewRPN(vars) + + result, err := rpnCalc.ParseAndEvaluate(tt.expression) + + if err != nil { + t.Fatalf("Evaluate(%q) returned error: %v", tt.expression, err) + } + + if result != tt.expected { + t.Errorf("Evaluate(%q) = %q, want %q", tt.expression, result, tt.expected) + } + }) + } +} diff --git a/internal/rpn/operations.go b/internal/rpn/operations.go index d9965ed..2b37c39 100644 --- a/internal/rpn/operations.go +++ b/internal/rpn/operations.go @@ -21,6 +21,16 @@ type ArithmeticOperator interface { Ln(stack *Stack) error } +// BooleanOperator defines the interface for boolean comparison operators. +type BooleanOperator interface { + GT(stack *Stack) error + LT(stack *Stack) error + GTE(stack *Stack) error + LTE(stack *Stack) error + EQ(stack *Stack) error + NEQ(stack *Stack) error +} + // HyperOperator defines the interface for hyper operators. type HyperOperator interface { HyperAdd(stack *Stack) error @@ -52,6 +62,7 @@ type VariableOperator interface { // This allows RPN to depend on an abstraction instead of the concrete Operations type. type Operator interface { ArithmeticOperator + BooleanOperator HyperOperator StackOperator VariableOperator @@ -107,6 +118,17 @@ func NewOperatorRegistry(op Operator) *OperatorRegistry { registry.registerStandardOperator("lg", func(stack *Stack) error { return op.Log2(stack) }) registry.registerStandardOperator("log", func(stack *Stack) error { return op.Log10(stack) }) registry.registerStandardOperator("ln", func(stack *Stack) error { return op.Ln(stack) }) + registry.registerStandardOperator("gt", func(stack *Stack) error { return op.GT(stack) }) + registry.registerStandardOperator("lt", func(stack *Stack) error { return op.LT(stack) }) + registry.registerStandardOperator(">", func(stack *Stack) error { return op.LT(stack) }) + registry.registerStandardOperator("gte", func(stack *Stack) error { return op.GTE(stack) }) + registry.registerStandardOperator(">=", func(stack *Stack) error { return op.GTE(stack) }) + registry.registerStandardOperator("lte", func(stack *Stack) error { return op.LTE(stack) }) + registry.registerStandardOperator("<=", func(stack *Stack) error { return op.LTE(stack) }) + registry.registerStandardOperator("eq", func(stack *Stack) error { return op.EQ(stack) }) + registry.registerStandardOperator("==", func(stack *Stack) error { return op.EQ(stack) }) + registry.registerStandardOperator("neq", func(stack *Stack) error { return op.NEQ(stack) }) + registry.registerStandardOperator("!=", func(stack *Stack) error { return op.NEQ(stack) }) registry.registerStandardOperator("dup", func(stack *Stack) error { return op.Dup(stack) }) registry.registerStandardOperator("swap", func(stack *Stack) error { return op.Swap(stack) }) registry.registerStandardOperator("pop", func(stack *Stack) error { return op.Pop(stack) }) @@ -626,6 +648,104 @@ func (o *Operations) HyperLn(stack *Stack) error { return nil } +// Boolean operators + +// GT pops two values from stack, compares (a > b), and pushes a boolean result. +func (o *Operations) 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(NewBoolValue(toNumber(a) > toNumber(b))) + return nil +} + +// LT pops two values from stack, compares (a < b), and pushes a boolean result. +func (o *Operations) 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(NewBoolValue(toNumber(a) < toNumber(b))) + return nil +} + +// GTE pops two values from stack, compares (a >= b), and pushes a boolean result. +func (o *Operations) 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(NewBoolValue(toNumber(a) >= toNumber(b))) + return nil +} + +// LTE pops two values from stack, compares (a <= b), and pushes a boolean result. +func (o *Operations) 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(NewBoolValue(toNumber(a) <= toNumber(b))) + return nil +} + +// EQ pops two values from stack, compares (a == b), and pushes a boolean result. +func (o *Operations) 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(NewBoolValue(toNumber(a) == toNumber(b))) + return nil +} + +// NEQ pops two values from stack, compares (a != b), and pushes a boolean result. +func (o *Operations) 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(NewBoolValue(toNumber(a) != toNumber(b))) + return nil +} + // stack manipulation operators // Dup duplicates the top stack value. diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go index b2a12dc..2f2bd9d 100644 --- a/internal/rpn/rpn.go +++ b/internal/rpn/rpn.go @@ -183,6 +183,16 @@ func (r *RPN) evaluate(tokens []string) (string, error) { return "", fmt.Errorf("rpn: invalid assignment syntax at token %d: 'name value =' requires spaces around =", i) } + // Check if it's a boolean literal + if token == "true" { + stack.Push(NewBoolValue(true)) + continue + } + if token == "false" { + stack.Push(NewBoolValue(false)) + continue + } + // Check if it's a number if num, err := strconv.ParseFloat(token, 64); err == nil { if stack.Len() >= r.maxStack { @@ -209,7 +219,7 @@ func (r *RPN) evaluate(tokens []string) (string, error) { // Create a copy of the stack to preserve it r.currentStack = NewStack() for _, val := range stack.Values() { - r.currentStack.Push(NewNumberValue(toNumber(val))) + r.currentStack.Push(val) } // Get the final result @@ -224,7 +234,7 @@ func (r *RPN) evaluate(tokens []string) (string, error) { // Single value - return it val, _ := stack.Pop() - return fmt.Sprintf("%.10g", toNumber(val)), nil + return val.String(), nil } // handleOperator handles operators and special commands using the operator registry. |
