diff options
| -rw-r--r-- | cmd/gt/main.go.bak | 148 | ||||
| -rw-r--r-- | internal/rpn/mode.go | 14 | ||||
| -rw-r--r-- | internal/rpn/rpn.go | 341 | ||||
| -rw-r--r-- | internal/rpn/rpn_ops.go | 97 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 198 | ||||
| -rw-r--r-- | internal/rpn/rpn_state.go | 61 |
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) + } +} |
