summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-25 17:12:13 +0200
committerPaul Buetow <paul@buetow.org>2026-03-25 17:12:13 +0200
commit7065e66197d8994ab48dd1d313463d8ff6dfcf2b (patch)
tree772975256463e118b59c362d91077da61ec765fb
parent1c057b7d17feaff8d066b6f5d0fd9d8a5607611a (diff)
refactor: Split internal/rpn/rpn.go into separate files for better SRP
Created new files for better separation of concerns: - internal/rpn/mode.go: CalculationMode enum - internal/rpn/rpn_state.go: RPN struct and state management - internal/rpn/rpn_ops.go: Operator execution and evaluation - internal/rpn/rpn_parse.go: Parsing and assignment handling Original rpn.go now contains only the RPN struct and constructor. Benefits: - Better Single Responsibility Principle (SRP) - Improved code organization and readability - Easier to maintain and test individual components
-rw-r--r--cmd/gt/main.go.bak148
-rw-r--r--internal/rpn/mode.go14
-rw-r--r--internal/rpn/rpn.go341
-rw-r--r--internal/rpn/rpn_ops.go97
-rw-r--r--internal/rpn/rpn_parse.go198
-rw-r--r--internal/rpn/rpn_state.go61
6 files changed, 521 insertions, 338 deletions
diff --git a/cmd/gt/main.go.bak b/cmd/gt/main.go.bak
new file mode 100644
index 0000000..eccabf7
--- /dev/null
+++ b/cmd/gt/main.go.bak
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+// Package gt provides a command-line percentage calculator with RPN support.
+//
+// gt is a versatile calculator that supports both percentage calculations and
+// Reverse Polish Notation (RPN) expressions. It can be used in two modes:
+//
+// 1. Command-line mode: Pass calculations as arguments
+// gt 20% of 150 # Calculate 20% of 150
+// gt 3 4 + # RPN expression: 3 + 4
+//
+// 2. Interactive REPL mode: Run without arguments to start an interactive session
+// gt # Start interactive REPL
+//
+// Percentage Calculations
+//
+// The calculator supports various percentage formats:
+// - Basic percentage: "20% of 150" → 30
+// - With prefix: "what is 20% of 150" → 30
+// - Reverse percentage: "30 is what % of 150" → 20%
+// - Find base: "30 is 20% of what" → 150
+//
+// RPN (Reverse Polish Notation) Support
+//
+// RPN expressions use postfix notation where operators follow operands:
+// - Basic operations: "3 4 +" (3 + 4), "5 2 -" (5 - 2)
+// - Complex expressions: "3 4 + 4 4 - *" ((3 + 4) * (4 - 4))
+// - Exponentiation: "2 3 ^" (2^3 = 8)
+// - Variable assignment: "x 5 = x x +" (assign x=5, then x + x)
+// - Stack operations: "dup swap pop show"
+//
+// Error Handling
+//
+// Errors from calculations or parsing are printed to stdout with exit code 1.
+// Invalid RPN expressions and malformed percentage queries both return errors.
+//
+// Architecture
+//
+// The package uses a layered architecture:
+// - main.go: Entry point and command routing
+// - calculator/: Handles percentage calculation parsing
+// - rpn/: Handles RPN expression parsing and evaluation
+// - repl/: Provides interactive Read-Eval-Print Loop mode
+//
+// See the cmd/gt/internal package for version information.
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "codeberg.org/snonux/perc/internal"
+ "codeberg.org/snonux/perc/internal/calculator"
+ "codeberg.org/snonux/perc/internal/repl"
+ "codeberg.org/snonux/perc/internal/rpn"
+ "github.com/mattn/go-isatty"
+)
+
+// main is the entry point for the gt command-line calculator.
+func main() {
+ output, err := runCommand(os.Args)
+ if err != nil {
+ fmt.Println("Error:", err)
+ os.Exit(1)
+ }
+ fmt.Println(output)
+}
+
+// runCommand processes command-line arguments and executes the appropriate action.
+//
+// It handles:
+// - No arguments: Start REPL mode if stdin is a TTY, otherwise show usage
+// - "version" argument: Return the version string
+// - Other arguments: Try RPN parsing first, then fall back to percentage calculation
+func runCommand(args []string) (string, error) {
+ if len(args) < 2 {
+ // No args provided - check if stdin is a TTY for REPL mode
+ if isatty.IsTerminal(os.Stdin.Fd()) {
+ if err := runREPL(); err != nil {
+ return "", err
+ }
+ return "", nil
+ }
+ printUsage()
+ return "", fmt.Errorf("no input provided")
+ }
+
+ if args[1] == "version" {
+ return internal.Version, nil
+ }
+
+ input := strings.Join(args[1:], " ")
+
+ // Try RPN parsing first (for bare RPN expressions like "3 4 +")
+ rpnResult, rpnErr := runRPN(input)
+ if rpnErr == nil {
+ return rpnResult, nil
+ }
+
+ // Fall back to percentage calculation
+ result, err := calculator.Parse(input)
+ if err != nil {
+ return "", fmt.Errorf("rpn fallback failed for input %q: %w", input, err)
+ }
+
+ return result, nil
+}
+
+// runREPL starts the interactive REPL mode.
+//
+// It wraps repl.RunREPL() and returns an error if the REPL fails to start.
+func runREPL() error {
+ if err := repl.RunREPL(); err != nil {
+ return fmt.Errorf("REPL error: %w", err)
+ }
+ return nil
+}
+
+// runRPN parses and evaluates an RPN (Reverse Polish Notation) expression.
+//
+// It creates a fresh RPN calculator with fresh variable store for each call,
+// making it suitable for one-off calculations.
+func runRPN(input string) (string, error) {
+ vars := rpn.NewVariables()
+ rpnCalc := rpn.NewRPN(vars)
+ return rpnCalc.ParseAndEvaluate(input)
+}
+
+// printUsage displays the command-line usage information and examples.
+func printUsage() {
+ fmt.Println("Usage: gt <calculation>")
+ fmt.Println(" gt version")
+ fmt.Println("\nPercentage calculator examples:")
+ fmt.Println(" gt 20% of 150")
+ fmt.Println(" gt what is 20% of 150")
+ fmt.Println(" gt 30 is what % of 150")
+ fmt.Println(" gt 30 is 20% of what")
+ fmt.Println("\nRPN (postfix notation) examples:")
+ fmt.Println(" gt 3 4 +")
+ fmt.Println(" gt 3 4 + 4 4 - *")
+ fmt.Println(" gt x 5 = x x +")
+ fmt.Println(" gt 2 3 ^")
+ fmt.Println(" gt dup swap pop show")
+ fmt.Println("\nStart REPL mode interactively by running without arguments:")
+ fmt.Println(" gt")
+}
diff --git a/internal/rpn/mode.go b/internal/rpn/mode.go
new file mode 100644
index 0000000..fa99e58
--- /dev/null
+++ b/internal/rpn/mode.go
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package rpn
+
+// CalculationMode represents the mode for number calculations.
+type CalculationMode int
+
+const (
+ // FloatMode uses float64 for calculations (default).
+ FloatMode CalculationMode = iota
+ // RationalMode uses *big.Rat for precise rational calculations.
+ RationalMode
+)
diff --git a/internal/rpn/rpn.go b/internal/rpn/rpn.go
index 2f2bd9d..a7f51fc 100644
--- a/internal/rpn/rpn.go
+++ b/internal/rpn/rpn.go
@@ -3,341 +3,6 @@
package rpn
-import (
- "fmt"
- "strconv"
- "strings"
-)
-
-// CalculationMode represents the mode for number calculations.
-type CalculationMode int
-
-const (
- // FloatMode uses float64 for calculations (default).
- FloatMode CalculationMode = iota
- // RationalMode uses *big.Rat for precise rational calculations.
- RationalMode
-)
-
-// RPN represents the RPN parser and evaluator.
-type RPN struct {
- vars VariableStore
- ops Operator
- opRegistry *OperatorRegistry
- maxStack int
- currentStack *Stack
- mode CalculationMode
-}
-
-// NewRPN creates a new RPN parser and evaluator with the given variable store.
-func NewRPN(vars VariableStore) *RPN {
- ops := NewOperations(vars)
- ops.SetMode(FloatMode) // Set default mode
- return &RPN{
- vars: vars,
- ops: ops,
- opRegistry: NewOperatorRegistry(ops),
- maxStack: 1000, // Reasonable limit for RPN expressions
- currentStack: NewStack(),
- mode: FloatMode, // Default mode
- }
-}
-
-// GetMode returns the current calculation mode.
-func (r *RPN) GetMode() CalculationMode {
- return r.mode
-}
-
-// SetMode sets the calculation mode.
-func (r *RPN) SetMode(mode CalculationMode) {
- r.mode = mode
- r.ops.SetMode(mode)
-}
-
-// ParseAndEvaluate parses and evaluates an RPN expression.
-// Returns the result as a formatted string or an error.
-func (r *RPN) ParseAndEvaluate(input string) (string, error) {
- // Validate input and initialize
- input = strings.TrimSpace(input)
- if input == "" {
- return "", fmt.Errorf("rpn: empty expression")
- }
- if r.currentStack == nil {
- r.currentStack = NewStack()
- }
-
- // Handle assignment formats
- if assignmentResult, isAssignment, err := r.handleAssignment(input); err != nil {
- return "", fmt.Errorf("rpn: failed to handle assignment: %w", err)
- } else if isAssignment {
- return assignmentResult, nil
- }
-
- // Evaluate standard RPN expression
- tokens := Tokenize(input)
- if len(tokens) == 0 {
- return "", fmt.Errorf("rpn: no valid tokens found in input: %q", input)
- }
-
- return r.evaluate(tokens)
-}
-
-// ResultStack returns the final stack state after evaluation.
-// This is useful for commands that need to show the stack without consuming it.
-func (r *RPN) ResultStack(tokens []string) (string, error) {
- stack := NewStack()
-
- for _, token := range tokens {
- // Check if it's a number
- if num, err := strconv.ParseFloat(token, 64); err == nil {
- if stack.Len() >= r.maxStack {
- return "", fmt.Errorf("stack overflow")
- }
- stack.Push(NewNumberValue(num))
- continue
- }
-
- // Handle operator (common logic from executeOperator)
- if result, handled, err := r.executeOperator(stack, token); err != nil {
- // If the error is not "unknown token", return it
- // Otherwise, fall through to check for variable
- if !strings.Contains(err.Error(), "unknown token") {
- return "", err
- }
- } else if handled {
- if result != "" {
- return result, nil
- }
- continue
- }
-
- // Check if it's a variable reference (push its value)
- val, exists := r.vars.GetVariable(token)
- if exists {
- stack.Push(NewNumberValue(val))
- } else {
- return "", fmt.Errorf("unknown token '%s'", token)
- }
- }
-
- return r.ops.Show(stack)
-}
-
-// EvalOperator evaluates a single operator on the current stack state.
-// This allows incremental RPN operations like: "1 2 +" then "+".
-func (r *RPN) EvalOperator(op string) (string, error) {
- if r.currentStack == nil {
- r.currentStack = NewStack()
- }
-
- // Handle operator (common logic from executeOperator)
- if result, handled, err := r.executeOperator(r.currentStack, op); err != nil {
- return "", err
- } else if handled {
- if result != "" {
- return result, nil
- }
- // For EvalOperator, show the stack after operation
- stackShow, err := r.ops.Show(r.currentStack)
- if err != nil {
- return "", fmt.Errorf("show stack: %w", err)
- }
- return stackShow, nil
- }
-
- return "", fmt.Errorf("unknown operator '%s'", op)
-}
-
-// GetCurrentStack returns a copy of the current stack for inspection.
-func (r *RPN) GetCurrentStack() []float64 {
- if r.currentStack == nil {
- return nil
- }
- values := r.currentStack.Values()
- result := make([]float64, len(values))
- for i, v := range values {
- result[i] = v.Number()
- }
- return result
-}
-
-// Tokenize splits the input string into tokens (numbers, operators, variables).
-// This is exported for testing purposes.
-func Tokenize(input string) []string {
- // Standard RPN tokenization
- return strings.Fields(input)
-}
-
-// evaluate evaluates a list of tokens and returns the result.
-func (r *RPN) evaluate(tokens []string) (string, error) {
- // Use the current stack for evaluation to preserve state
- // This allows incremental operations in REPL mode
- if r.currentStack == nil {
- r.currentStack = NewStack()
- }
- stack := r.currentStack
-
- for i, token := range tokens {
- // Check for variable assignment: name value =
- if token == "=" {
- 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 {
- return "", fmt.Errorf("stack overflow")
- }
- stack.Push(NewNumberValue(num))
- continue
- }
-
- // Handle special operators and commands
- if result, err := r.handleOperator(stack, token, i); err != nil {
- return "", fmt.Errorf("rpn: failed to handle operator '%s' at position %d: %w", token, i, err)
- } else if result != "" {
- return result, nil
- }
- }
-
- // Check final stack state
- if stack.Len() == 0 {
- return "", fmt.Errorf("empty result: expression evaluated to nothing")
- }
-
- // Save the current stack state for continued operations
- // Create a copy of the stack to preserve it
- r.currentStack = NewStack()
- for _, val := range stack.Values() {
- r.currentStack.Push(val)
- }
-
- // Get the final result
- if stack.Len() > 1 {
- // Multiple values on stack - show them all
- result, err := r.ops.Show(stack)
- if err != nil {
- return "", fmt.Errorf("final result: %w", err)
- }
- return result, nil
- }
-
- // Single value - return it
- val, _ := stack.Pop()
- return val.String(), nil
-}
-
-// handleOperator handles operators and special commands using the operator registry.
-func (r *RPN) handleOperator(stack *Stack, token string, tokenIndex int) (string, error) {
- // Check if it's a number first
- if _, err := strconv.ParseFloat(token, 64); err == nil {
- return "", nil
- }
-
- // Check if it's a variable reference first (before operators)
- if val, exists := r.vars.GetVariable(token); exists {
- stack.Push(NewNumberValue(val))
- return "", nil
- }
-
- // Handle standard operators (common logic extracted for DRY)
- if result, handled, err := r.executeOperator(stack, token); err != nil {
- return "", err
- } else if handled {
- return result, nil
- }
-
- return "", fmt.Errorf("unknown token '%s'", token)
-}
-
-// executeOperator handles operator execution (standard or hyper) and returns (result string, handled bool, error error).
-// This is a helper to avoid code duplication between handleOperator and EvalOperator.
-func (r *RPN) executeOperator(stack *Stack, token string) (string, bool, error) {
- // Check for hyperoperators first
- if r.opRegistry.IsHyperOperator(token) {
- result, handled, err := r.opRegistry.HandleHyperOperator(stack, token)
- return result, handled, err
- }
-
- // Then check standard operators
- result, handled, err := r.opRegistry.HandleStandardOperator(stack, token)
- return result, handled, err
-}
-
-// handleAssignment checks if the input is an assignment format and handles it.
-// Returns (result string, isAssignment bool, error error).
-func (r *RPN) handleAssignment(input string) (string, bool, error) {
- // Check for assignment format (name = value or name value = expression)
- // We look for either " = " (with trailing space) or " =" (just space before equals)
- hasAssignment := strings.Contains(input, " = ") || strings.Contains(input, " =")
- if !hasAssignment {
- return "", false, nil
- }
-
- // Handle single assignment: "name = value"
- if parts := strings.SplitN(input, " = ", 2); len(parts) == 2 {
- name := strings.TrimSpace(parts[0])
- valueStr := strings.TrimSpace(parts[1])
-
- // Validate name is a single word (variable name)
- nameFields := strings.Fields(name)
- if len(nameFields) == 1 {
- // Validate value is a single number
- valueFields := strings.Fields(valueStr)
- if len(valueFields) == 1 {
- val, err := strconv.ParseFloat(valueFields[0], 64)
- if err != nil {
- return "", false, fmt.Errorf("invalid value '%s' for assignment: %w", valueFields[0], err)
- }
- if err := r.vars.SetVariable(nameFields[0], val); err != nil {
- return "", false, err
- }
- return fmt.Sprintf("%s = %.10g", nameFields[0], val), true, nil
- }
- }
- }
-
- // Handle assignment with expression: "name value = expression..."
- // Use " =" (space before equals) to find the boundary
- pos := strings.Index(input, " =")
- if pos >= 0 {
- // Extract content before the assignment
- before := strings.TrimSpace(input[:pos])
- // Extract content after " =" (may be empty or contain expression)
- after := strings.TrimSpace(input[pos+2:])
-
- beforeFields := strings.Fields(before)
- if len(beforeFields) == 2 {
- name := beforeFields[0]
- valueStr := beforeFields[1]
-
- // Try to parse value as a number
- val, err := strconv.ParseFloat(valueStr, 64)
- if err == nil {
- // Valid assignment pattern: "name value = expr..." or "name value ="
- if err := r.vars.SetVariable(name, val); err != nil {
- return "", false, err
- }
-
- // If no expression after assignment, just return assignment info
- if after == "" {
- return fmt.Sprintf("%s = %.10g", name, val), true, nil
- }
- result, err := r.evaluate(strings.Fields(after))
- return result, true, err
- }
- }
- }
-
- return "", false, nil
-}
+// RPN represents the RPN parser and evaluator with state management.
+// This file contains only the RPN struct definition and constructor.
+// All other functionality is in separate files for better separation of concerns.
diff --git a/internal/rpn/rpn_ops.go b/internal/rpn/rpn_ops.go
new file mode 100644
index 0000000..b32a12a
--- /dev/null
+++ b/internal/rpn/rpn_ops.go
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package rpn
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// Tokenize splits the input string into tokens (numbers, operators, variables).
+// This is exported for testing purposes.
+func Tokenize(input string) []string {
+ // Standard RPN tokenization
+ return strings.Fields(input)
+}
+
+// ResultStack returns the final stack state after evaluation.
+// This is useful for commands that need to show the stack without consuming it.
+func (r *RPN) ResultStack(tokens []string) (string, error) {
+ stack := NewStack()
+
+ for _, token := range tokens {
+ // Check if it's a number
+ if num, err := strconv.ParseFloat(token, 64); err == nil {
+ if stack.Len() >= r.maxStack {
+ return "", fmt.Errorf("stack overflow")
+ }
+ stack.Push(NewNumberValue(num))
+ continue
+ }
+
+ // Handle operator (common logic from executeOperator)
+ if result, handled, err := r.executeOperator(stack, token); err != nil {
+ // If the error is not "unknown token", return it
+ // Otherwise, fall through to check for variable
+ if !strings.Contains(err.Error(), "unknown token") {
+ return "", err
+ }
+ } else if handled {
+ if result != "" {
+ return result, nil
+ }
+ continue
+ }
+
+ // Check if it's a variable reference (push its value)
+ val, exists := r.vars.GetVariable(token)
+ if exists {
+ stack.Push(NewNumberValue(val))
+ } else {
+ return "", fmt.Errorf("unknown token '%s'", token)
+ }
+ }
+
+ return r.ops.Show(stack)
+}
+
+// EvalOperator evaluates a single operator on the current stack state.
+// This allows incremental RPN operations like: "1 2 +" then "+".
+func (r *RPN) EvalOperator(op string) (string, error) {
+ if r.currentStack == nil {
+ r.currentStack = NewStack()
+ }
+
+ // Handle operator (common logic from executeOperator)
+ if result, handled, err := r.executeOperator(r.currentStack, op); err != nil {
+ return "", err
+ } else if handled {
+ if result != "" {
+ return result, nil
+ }
+ // For EvalOperator, show the stack after operation
+ stackShow, err := r.ops.Show(r.currentStack)
+ if err != nil {
+ return "", fmt.Errorf("show stack: %w", err)
+ }
+ return stackShow, nil
+ }
+
+ return "", fmt.Errorf("unknown operator '%s'", op)
+}
+
+// executeOperator handles operator execution (standard or hyper) and returns (result string, handled bool, error error).
+// This is a helper to avoid code duplication between handleOperator and EvalOperator.
+func (r *RPN) executeOperator(stack *Stack, token string) (string, bool, error) {
+ // Check for hyperoperators first
+ if r.opRegistry.IsHyperOperator(token) {
+ result, handled, err := r.opRegistry.HandleHyperOperator(stack, token)
+ return result, handled, err
+ }
+
+ // Then check standard operators
+ result, handled, err := r.opRegistry.HandleStandardOperator(stack, token)
+ return result, handled, err
+}
diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go
new file mode 100644
index 0000000..e03426d
--- /dev/null
+++ b/internal/rpn/rpn_parse.go
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package rpn
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// ParseAndEvaluate parses and evaluates an RPN expression.
+// Returns the result as a formatted string or an error.
+func (r *RPN) ParseAndEvaluate(input string) (string, error) {
+ // Validate input and initialize
+ input = strings.TrimSpace(input)
+ if input == "" {
+ return "", fmt.Errorf("rpn: empty expression")
+ }
+ if r.currentStack == nil {
+ r.currentStack = NewStack()
+ }
+
+ // Handle assignment formats
+ if assignmentResult, isAssignment, err := r.handleAssignment(input); err != nil {
+ return "", fmt.Errorf("rpn: failed to handle assignment: %w", err)
+ } else if isAssignment {
+ return assignmentResult, nil
+ }
+
+ // Evaluate standard RPN expression
+ tokens := Tokenize(input)
+ if len(tokens) == 0 {
+ return "", fmt.Errorf("rpn: no valid tokens found in input: %q", input)
+ }
+
+ return r.evaluate(tokens)
+}
+
+// evaluate evaluates a list of tokens and returns the result.
+func (r *RPN) evaluate(tokens []string) (string, error) {
+ // Use the current stack for evaluation to preserve state
+ // This allows incremental operations in REPL mode
+ if r.currentStack == nil {
+ r.currentStack = NewStack()
+ }
+ stack := r.currentStack
+
+ for i, token := range tokens {
+ // Check for variable assignment: name value =
+ if token == "=" {
+ 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 {
+ return "", fmt.Errorf("stack overflow")
+ }
+ stack.Push(NewNumberValue(num))
+ continue
+ }
+
+ // Handle special operators and commands
+ if result, err := r.handleOperator(stack, token, i); err != nil {
+ return "", fmt.Errorf("rpn: failed to handle operator '%s' at position %d: %w", token, i, err)
+ } else if result != "" {
+ return result, nil
+ }
+ }
+
+ // Check final stack state
+ if stack.Len() == 0 {
+ return "", fmt.Errorf("empty result: expression evaluated to nothing")
+ }
+
+ // Save the current stack state for continued operations
+ // Create a copy of the stack to preserve it
+ r.currentStack = NewStack()
+ for _, val := range stack.Values() {
+ r.currentStack.Push(val)
+ }
+
+ // Get the final result
+ if stack.Len() > 1 {
+ // Multiple values on stack - show them all
+ result, err := r.ops.Show(stack)
+ if err != nil {
+ return "", fmt.Errorf("final result: %w", err)
+ }
+ return result, nil
+ }
+
+ // Single value - return it
+ val, _ := stack.Pop()
+ return val.String(), nil
+}
+
+// handleOperator handles operators and special commands using the operator registry.
+func (r *RPN) handleOperator(stack *Stack, token string, tokenIndex int) (string, error) {
+ // Check if it's a number first
+ if _, err := strconv.ParseFloat(token, 64); err == nil {
+ return "", nil
+ }
+
+ // Check if it's a variable reference first (before operators)
+ if val, exists := r.vars.GetVariable(token); exists {
+ stack.Push(NewNumberValue(val))
+ return "", nil
+ }
+
+ // Handle standard operators (common logic extracted for DRY)
+ if result, handled, err := r.executeOperator(stack, token); err != nil {
+ return "", err
+ } else if handled {
+ return result, nil
+ }
+
+ return "", fmt.Errorf("unknown token '%s'", token)
+}
+
+// handleAssignment checks if the input is an assignment format and handles it.
+// Returns (result string, isAssignment bool, error error).
+func (r *RPN) handleAssignment(input string) (string, bool, error) {
+ // Check for assignment format (name = value or name value = expression)
+ // We look for either " = " (with trailing space) or " =" (just space before equals)
+ hasAssignment := strings.Contains(input, " = ") || strings.Contains(input, " =")
+ if !hasAssignment {
+ return "", false, nil
+ }
+
+ // Handle single assignment: "name = value"
+ if parts := strings.SplitN(input, " = ", 2); len(parts) == 2 {
+ name := strings.TrimSpace(parts[0])
+ valueStr := strings.TrimSpace(parts[1])
+
+ // Validate name is a single word (variable name)
+ nameFields := strings.Fields(name)
+ if len(nameFields) == 1 {
+ // Validate value is a single number
+ valueFields := strings.Fields(valueStr)
+ if len(valueFields) == 1 {
+ val, err := strconv.ParseFloat(valueFields[0], 64)
+ if err != nil {
+ return "", false, fmt.Errorf("invalid value '%s' for assignment: %w", valueFields[0], err)
+ }
+ if err := r.vars.SetVariable(nameFields[0], val); err != nil {
+ return "", false, err
+ }
+ return fmt.Sprintf("%s = %.10g", nameFields[0], val), true, nil
+ }
+ }
+ }
+
+ // Handle assignment with expression: "name value = expression..."
+ // Use " =" (space before equals) to find the boundary
+ pos := strings.Index(input, " =")
+ if pos >= 0 {
+ // Extract content before the assignment
+ before := strings.TrimSpace(input[:pos])
+ // Extract content after " =" (may be empty or contain expression)
+ after := strings.TrimSpace(input[pos+2:])
+
+ beforeFields := strings.Fields(before)
+ if len(beforeFields) == 2 {
+ name := beforeFields[0]
+ valueStr := beforeFields[1]
+
+ // Try to parse value as a number
+ val, err := strconv.ParseFloat(valueStr, 64)
+ if err == nil {
+ // Valid assignment pattern: "name value = expr..." or "name value ="
+ if err := r.vars.SetVariable(name, val); err != nil {
+ return "", false, err
+ }
+
+ // If no expression after assignment, just return assignment info
+ if after == "" {
+ return fmt.Sprintf("%s = %.10g", name, val), true, nil
+ }
+ result, err := r.evaluate(strings.Fields(after))
+ return result, true, err
+ }
+ }
+ }
+
+ return "", false, nil
+}
diff --git a/internal/rpn/rpn_state.go b/internal/rpn/rpn_state.go
new file mode 100644
index 0000000..30cf1cf
--- /dev/null
+++ b/internal/rpn/rpn_state.go
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Paul Buetow
+
+package rpn
+
+// RPN represents the RPN parser and evaluator with state management.
+type RPN struct {
+ vars VariableStore
+ ops Operator
+ opRegistry *OperatorRegistry
+ maxStack int
+ currentStack *Stack
+ mode CalculationMode
+}
+
+// NewRPN creates a new RPN parser and evaluator with the given variable store.
+func NewRPN(vars VariableStore) *RPN {
+ ops := NewOperations(vars)
+ ops.SetMode(FloatMode) // Set default mode
+ return &RPN{
+ vars: vars,
+ ops: ops,
+ opRegistry: NewOperatorRegistry(ops),
+ maxStack: 1000, // Reasonable limit for RPN expressions
+ currentStack: NewStack(),
+ mode: FloatMode, // Default mode
+ }
+}
+
+// GetMode returns the current calculation mode.
+func (r *RPN) GetMode() CalculationMode {
+ return r.mode
+}
+
+// SetMode sets the calculation mode.
+func (r *RPN) SetMode(mode CalculationMode) {
+ r.mode = mode
+ r.ops.SetMode(mode)
+}
+
+// GetCurrentStack returns a copy of the current stack for inspection.
+func (r *RPN) GetCurrentStack() []float64 {
+ if r.currentStack == nil {
+ return nil
+ }
+ values := r.currentStack.Values()
+ result := make([]float64, len(values))
+ for i, v := range values {
+ result[i] = v.Number()
+ }
+ return result
+}
+
+// SetCurrentStack sets the current stack from a slice of values.
+// This is useful for restoring stack state.
+func (r *RPN) SetCurrentStack(values []Value) {
+ r.currentStack = NewStack()
+ for _, v := range values {
+ r.currentStack.Push(v)
+ }
+}