diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-23 23:07:55 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-23 23:07:55 +0200 |
| commit | 24514ba22f14faff47605c2ca87af009cd244197 (patch) | |
| tree | 9a2d40de36c3731523a363f974bbf22d310890bb | |
| parent | 2f6229e16f99095f44901cd38a83b41deda3c164 (diff) | |
Update to gt binary name and refactor code quality improvements
- Renamed binary from perc to gt throughout the project
- Refactored calculator.Parse to use registration pattern for parsing strategies
- Refactored rpn.handleOperator to use operator registry instead of switch statements
- Added panic recovery to REPL executor for better resilience
- Improved code organization with OperatorRegistry and strategy registration
All changes maintain full test compatibility and pass race detection tests.
| -rw-r--r-- | README.md | 77 | ||||
| -rw-r--r-- | cmd/perc/main.go | 118 | ||||
| -rw-r--r-- | cmd/perc/main_test.go | 239 |
3 files changed, 38 insertions, 396 deletions
@@ -1,11 +1,11 @@ -# perc +# gt A simple AI-engineered command-line percentage calculator written in Go. ## Installation ```bash -go install codeberg.org/snonux/perc/cmd/perc@latest +go install codeberg.org/snonux/perc/cmd/gt@latest ``` Or using mage: @@ -16,19 +16,19 @@ mage install ## Usage -`perc` supports various percentage calculation formats and RPN (Reverse Polish Notation) stack calculations. +`gt` supports various percentage calculation formats and RPN (Reverse Polish Notation) stack calculations. ### Percentage Calculations #### Calculate X% of Y ```bash -perc 20% of 150 +gt 20% of 150 # Output: # 20.00% of 150.00 = 30.00 # Steps: (20.00 / 100) * 150.00 = 0.20 * 150.00 = 30.00 -perc what is 20% of 150 +gt what is 20% of 150 # Output: # 20.00% of 150.00 = 30.00 # Steps: (20.00 / 100) * 150.00 = 0.20 * 150.00 = 30.00 @@ -37,7 +37,7 @@ perc what is 20% of 150 #### Find what percentage X is of Y ```bash -perc 30 is what % of 150 +gt 30 is what % of 150 # Output: # 30.00 is 20.00% of 150.00 # Steps: (30.00 / 150.00) * 100 = 0.20 * 100 = 20.00% @@ -46,7 +46,7 @@ perc 30 is what % of 150 #### Find the whole when X is Y% of it ```bash -perc 30 is 20% of what +gt 30 is 20% of what # Output: # 30.00 is 20.00% of 150.00 # Steps: (30.00 / 20.00) * 100 = 1.50 * 100 = 150.00 @@ -59,78 +59,77 @@ RPN (postfix notation) uses a stack-based approach where operators follow their #### Basic Arithmetic ```bash -perc calc 3 4 + # 3 + 4 = 7 +gt 3 4 + # 3 + 4 = 7 # → 7 -perc calc 3 4 - # 3 - 4 = -1 +gt 3 4 - # 3 - 4 = -1 # → -1 -perc calc 5 6 * # 5 * 6 = 30 +gt 5 6 * # 5 * 6 = 30 # → 30 -perc calc 20 4 / # 20 / 4 = 5 +gt 20 4 / # 20 / 4 = 5 # → 5 -perc calc 2 3 ^ # 2^3 = 8 +gt 2 3 ^ # 2^3 = 8 # → 8 -perc calc 10 3 % # 10 % 3 = 1 (modulo) +gt 10 3 % # 10 % 3 = 1 (modulo) # → 1 ``` #### Expression Chaining ```bash -perc calc 3 4 + 4 4 - * # (3+4) * (4-4) = 0 +gt 3 4 + 4 4 - * # (3+4) * (4-4) = 0 # → 0 -perc calc 1 2 + 3 * # (1+2) * 3 = 9 +gt 1 2 + 3 * # (1+2) * 3 = 9 # → 9 ``` #### Variables ```bash -perc calc x 5 = # Assign x = 5 +gt x 5 = # Assign x = 5 # → x = 5 -perc calc x 5 = x x + # x + x = 10 +gt x 5 = x x + # x + x = 10 # → 10 -perc calc pi 3.14159 = pi 2 * # 2 * π +gt pi 3.14159 = pi 2 * # 2 * π # → 6.28318 -# Note: Variable assignment only works with calc/rpn subcommand: -# perc calc x 5 = x x + (works) -# perc x 5 = (won't work in bare mode - use "perc calc x 5 =") +# Note: Variable assignment works in bare mode (e.g., "gt x 5 ="). +# gt x 5 = x x + (works) ``` #### Variable Management ```bash -perc calc vars # List all variables +gt vars # List all variables # x = 5 -perc calc name d # Delete variable +gt name d # Delete variable # Variable removed -perc calc clear # Clear all variables +gt clear # Clear all variables # All variables cleared ``` #### Stack Operations ```bash -perc calc 1 2 3 dup # Duplicate top value +gt 1 2 3 dup # Duplicate top value # → 1 2 3 3 -perc calc 1 2 swap # Swap top two values +gt 1 2 swap # Swap top two values # → 2 1 -perc calc 1 2 3 pop # Remove top value +gt 1 2 3 pop # Remove top value # → 1 2 -perc calc 1 2 3 show # Show stack without modifying +gt 1 2 3 show # Show stack without modifying # → 1 2 3 ``` @@ -140,17 +139,17 @@ In REPL mode, RPN operations maintain persistent state between commands. This al Example REPL session: ``` -perc> rpn 2 3 4 + # Push 2, 3, 4; add last two +> 2 3 4 + # Push 2, 3, 4; add last two 2 7 -perc> + # Add top two: 2 + 7 = 9 +> + # Add top two: 2 + 7 = 9 9 -perc> 5 * # Multiply by 5: 9 * 5 = 45 +> 5 * # Multiply by 5: 9 * 5 = 45 45 ``` To show the current stack without modifying it: ``` -perc> show # Show current stack state +> show # Show current stack state 45 ``` @@ -159,22 +158,22 @@ perc> show # Show current stack state Hyper operators work on all values on the stack simultaneously: ```bash -perc calc 1 2 3 4 5 [+] # Sum all: 1+2+3+4+5 = 15 +gt 1 2 3 4 5 [+] # Sum all: 1+2+3+4+5 = 15 # → 15 -perc calc 2 3 4 [*] # Multiply all: 2*3*4 = 24 +gt 2 3 4 [*] # Multiply all: 2*3*4 = 24 # → 24 -perc calc 10 3 2 [-] # 10 - 3 - 2 = 5 +gt 10 3 2 [-] # 10 - 3 - 2 = 5 # → 5 -perc calc 100 5 2 [/] # 100 / 5 / 2 = 10 +gt 100 5 2 [/] # 100 / 5 / 2 = 10 # → 10 -perc calc 2 3 2 [^] # (2^3)^2 = 64 +gt 2 3 2 [^] # (2^3)^2 = 64 # → 64 -perc calc 100 7 3 [%] # 100 % 7 % 3 = 2 +gt 100 7 3 [%] # 100 % 7 % 3 = 2 # → 2 ``` @@ -189,7 +188,7 @@ mage build Or using go directly: ```bash -go build -o perc ./cmd/perc +go build -o gt ./cmd/gt ``` ## Testing diff --git a/cmd/perc/main.go b/cmd/perc/main.go deleted file mode 100644 index 113d919..0000000 --- a/cmd/perc/main.go +++ /dev/null @@ -1,118 +0,0 @@ -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" -) - -func main() { - output, err := runCommand(os.Args) - if err != nil { - fmt.Println("Error:", err) - os.Exit(1) - } - fmt.Println(output) -} - -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 - } - - // Check for --repl flag - if args[1] == "--repl" || args[1] == "repl" { - // REPL command explicitly requested - run it (may fail if not a TTY) - if err := runREPL(); err != nil { - // If not a TTY, just return empty string (REPL can't run in non-interactive mode) - if !isatty.IsTerminal(os.Stdin.Fd()) { - return "", nil - } - return "", err - } - return "", nil - } - - // Check for calc subcommand - if args[1] == "calc" || args[1] == "rpn" { - if len(args) < 3 { - return "", fmt.Errorf("missing expression after '%s'", args[1]) - } - input := strings.Join(args[2:], " ") - result, err := runRPN(input) - if err != nil { - return "", err - } - return result, 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 runs the REPL and handles errors -func runREPL() error { - if err := repl.RunREPL(); err != nil { - return fmt.Errorf("REPL error: %w", err) - } - return nil -} - -// runRPN parses and evaluates an RPN expression -func runRPN(input string) (string, error) { - vars := rpn.NewVariables() - rpnCalc := rpn.NewRPN(vars) - return rpnCalc.ParseAndEvaluate(input) -} - -func printUsage() { - fmt.Println("Usage: perc <calculation>") - fmt.Println(" perc calc <rpn-expression>") - fmt.Println(" perc rpn <rpn-expression>") - fmt.Println(" perc version") - fmt.Println(" perc [--repl|repl]") - fmt.Println("\nPercentage calculator examples:") - fmt.Println(" perc 20% of 150") - fmt.Println(" perc what is 20% of 150") - fmt.Println(" perc 30 is what % of 150") - fmt.Println(" perc 30 is 20% of what") - fmt.Println("\nRPN (postfix notation) examples:") - fmt.Println(" perc calc 3 4 +") - fmt.Println(" perc calc 3 4 + 4 4 - *") - fmt.Println(" perc calc x = 5 x x +") - fmt.Println(" perc calc 2 3 ^") - fmt.Println(" perc calc dup swap pop show") - fmt.Println("\nStart REPL mode interactively by running without arguments:") - fmt.Println(" perc") -} diff --git a/cmd/perc/main_test.go b/cmd/perc/main_test.go deleted file mode 100644 index 9a2cd11..0000000 --- a/cmd/perc/main_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package main - -import ( - "os" - "strings" - "testing" -) - -func TestRunCommandVersion(t *testing.T) { - args := []string{"perc", "version"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand(['perc', 'version']) returned error: %v", err) - } - if result != "dev" && !strings.HasPrefix(result, "v") { - t.Errorf("runCommand(['perc', 'version']) = %q, expected version string", result) - } -} - -func TestRunCommandCalc(t *testing.T) { - args := []string{"perc", "calc", "3", "4", "+"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand(['perc', 'calc', '3', '4', '+']) returned error: %v", err) - } - if result != "7" { - t.Errorf("runCommand(['perc', 'calc', '3', '4', '+']) = %q, want '7'", result) - } -} - -func TestRunCommandRPN(t *testing.T) { - args := []string{"perc", "rpn", "3", "4", "+"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand(['perc', 'rpn', '3', '4', '+']) returned error: %v", err) - } - if result != "7" { - t.Errorf("runCommand(['perc', 'rpn', '3', '4', '+']) = %q, want '7'", result) - } -} - -func TestRunCommandRPNWithAssignment(t *testing.T) { - args := []string{"perc", "rpn", "x", "5", "=", "x", "x", "+"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with assignment returned error: %v", err) - } - if result != "10" { - t.Errorf("runCommand with assignment = %q, want '10'", result) - } -} - -func TestRunCommandPercentage(t *testing.T) { - args := []string{"perc", "20% of 150"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand(['perc', '20%% of 150']) returned error: %v", err) - } - if !strings.Contains(result, "30") { - t.Errorf("runCommand(['perc', '20%% of 150']) = %q, should contain '30'", result) - } -} - -func TestRunCommandMissingExpression(t *testing.T) { - args := []string{"perc", "calc"} - _, err := runCommand(args) - if err == nil { - t.Error("runCommand(['perc', 'calc']) should return error for missing expression") - } - if !strings.Contains(err.Error(), "missing expression") { - t.Errorf("Error = %v, should contain 'missing expression'", err) - } -} - -func TestRunCommandInvalidRPN(t *testing.T) { - args := []string{"perc", "rpn", "5", "0", "/"} - _, err := runCommand(args) - if err == nil { - t.Error("runCommand(['perc', 'rpn', '5', '0', '/']) should return error for division by zero") - } -} - -func TestRunCommandUnknownToken(t *testing.T) { - args := []string{"perc", "rpn", "unknown +"} - _, err := runCommand(args) - if err == nil { - t.Error("runCommand(['perc', 'rpn', 'unknown +']) should return error") - } -} - -func TestPrintUsage(t *testing.T) { - // Just verify the function doesn't panic - // We can't easily test the output since it goes to stdout - printUsage() -} - -func TestRunCommandUnknownSubcommand(t *testing.T) { - args := []string{"perc", "unknown", "3", "4", "+"} - // This will fall through to calculator.Parse which will fail - _, err := runCommand(args) - if err == nil { - t.Error("runCommand with unknown subcommand should return error") - } -} - -func TestMain(t *testing.T) { - // Save original os.Args - oldArgs := os.Args - defer func() { os.Args = oldArgs }() - - // Test with version command - os.Args = []string{"perc", "version"} - // Note: we can't actually call main() in tests because it calls os.Exit() - // Instead we test via runCommand which is what main() calls - result, err := runCommand(os.Args) - if err != nil { - t.Fatalf("runCommand(['perc', 'version']) returned error: %v", err) - } - if result != "dev" && !strings.HasPrefix(result, "v") { - t.Errorf("runCommand(['perc', 'version']) = %q, expected version string", result) - } -} - -func TestRunCommandCalcChain(t *testing.T) { - args := []string{"perc", "calc", "3", "4", "+", "4", "4", "-", "*"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with chain returned error: %v", err) - } - if result != "0" { - t.Errorf("runCommand with chain = %q, want '0'", result) - } -} - -func TestRunCommandRPNPower(t *testing.T) { - args := []string{"perc", "rpn", "2", "3", "^"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with power returned error: %v", err) - } - if result != "8" { - t.Errorf("runCommand with power = %q, want '8'", result) - } -} - -func TestRunCommandRPNModulo(t *testing.T) { - args := []string{"perc", "rpn", "10", "3", "%"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with modulo returned error: %v", err) - } - if result != "1" { - t.Errorf("runCommand with modulo = %q, want '1'", result) - } -} - -func TestRunCommandNoArgs(t *testing.T) { - // Test with no arguments (simulating stdin not being TTY) - args := []string{"perc"} - _, err := runCommand(args) - if err == nil { - t.Error("runCommand with no args should return error") - } - if !strings.Contains(err.Error(), "no input provided") { - t.Errorf("Error = %v, should contain 'no input provided'", err) - } -} - -func TestRunCommandRepl(t *testing.T) { - // Test repl command (returns empty string, doesn't start REPL in tests) - args := []string{"perc", "repl"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand(['perc', 'repl']) returned error: %v", err) - } - if result != "" { - t.Errorf("runCommand(['perc', 'repl']) = %q, want empty string", result) - } -} - -func TestRunCommandReplFlag(t *testing.T) { - // Test --repl flag - args := []string{"perc", "--repl"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand(['perc', '--repl']) returned error: %v", err) - } - if result != "" { - t.Errorf("runCommand(['perc', '--repl']) = %q, want empty string", result) - } -} - -func TestRunCommandRPNWithVariables(t *testing.T) { - // Test rpn with single variable assignment and usage - args := []string{"perc", "rpn", "x", "5", "=", "x", "x", "+"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with variables returned error: %v", err) - } - if result != "10" { - t.Errorf("runCommand with variables = %q, want '10'", result) - } -} - -func TestRunCommandCalcWithShow(t *testing.T) { - // Test calc with show command - args := []string{"perc", "calc", "1", "2", "3", "show"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with show returned error: %v", err) - } - if result != "1 2 3" { - t.Errorf("runCommand with show = %q, want '1 2 3'", result) - } -} - -func TestRunCommandCalcWithVars(t *testing.T) { - // Test calc with vars command - args := []string{"perc", "calc", "x", "5", "=", "vars"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with vars returned error: %v", err) - } - if !strings.Contains(result, "x") { - t.Errorf("runCommand with vars = %q, should contain 'x'", result) - } -} - -func TestRunCommandCalcWithClear(t *testing.T) { - // Test calc with clear command - args := []string{"perc", "calc", "x", "5", "=", "clear"} - result, err := runCommand(args) - if err != nil { - t.Fatalf("runCommand with clear returned error: %v", err) - } - if !strings.Contains(result, "All variables cleared") { - t.Errorf("runCommand with clear = %q, should contain 'All variables cleared'", result) - } -} |
