52 Commits

Author SHA1 Message Date
43fcf43d1f add time support 2024-11-18 13:18:51 +01:00
3a9d753720 bump version 2024-10-02 10:46:39 +02:00
5afe1275bc implemented #12: added toggle commands like togglebatch 2024-10-02 10:45:41 +02:00
dependabot[bot]
41b38191a5 Bump github.com/rogpeppe/go-internal from 1.11.0 to 1.13.1
Bumps [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) from 1.11.0 to 1.13.1.
- [Release notes](https://github.com/rogpeppe/go-internal/releases)
- [Commits](https://github.com/rogpeppe/go-internal/compare/v1.11.0...v1.13.1)

---
updated-dependencies:
- dependency-name: github.com/rogpeppe/go-internal
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-02 10:41:09 +02:00
8f2b6955ff fix spelling 2024-09-25 19:34:35 +02:00
9b244fc170 do not use formatter, specify build target 2024-09-25 19:29:49 +02:00
e4b2a4d6ea update lua 2024-09-25 19:24:17 +02:00
3ee4d4181a Merge branch 'master' of github.com:TLINDEN/rpnc 2024-09-25 19:20:51 +02:00
1a1670076a bump version, use go 1.22 2024-09-25 19:20:22 +02:00
7ccb05558f add dependabot 2024-09-25 19:18:55 +02:00
T.v.Dein
b38b431d29 Add demo mp4 2024-05-12 21:24:06 +02:00
62188dda0c fix linter errors 2024-01-26 13:10:15 +01:00
6a2a501e48 fix printing of fractionals (not scientific anymore), added -p flag 2024-01-26 08:19:01 +01:00
T.v.Dein
e81be12b19 Merge pull request #31 from TLINDEN/internal/fuzzytesting
* reorganized Eval() to return errors and call EvalItem() on each item
* fix negative shift amount error, found with fuzzy testing :)
* added fuzzy testing
2023-12-08 18:43:10 +01:00
222dc3a734 added fuzzy testing 2023-12-08 18:37:59 +01:00
49e01565b9 catch exec errors 2023-12-08 18:37:35 +01:00
e4a8af9b5b fix negative shift amount error, found with fuzzy testing :) 2023-12-08 18:36:33 +01:00
ac9d08d6fc reorganized Eval() return errors and call EvalItem() on each item 2023-12-08 18:35:56 +01:00
T.v.Dein
cb774b3b80 added commandline and stdin tests using testscript (#28)
* added commandline and stdin tests using testscript

---------

Co-authored-by: Thomas von Dein <tom@vondein.org>
2023-12-07 14:09:42 +01:00
T.v.Dein
846b3e63fc don't show shortcuts in help (clutters it) (#27)
* don't show shortcuts in help (clutters it)

* bump version

---------

Co-authored-by: Thomas von Dein <tom@vondein.org>
2023-12-07 13:47:32 +01:00
T.v.Dein
5557ad5f99 use generics for contains() and add generic exists() (#29)
Co-authored-by: Thomas von Dein <tom@vondein.org>
2023-12-07 13:47:04 +01:00
T.v.Dein
d2db420837 Merge pull request #25 from TLINDEN/doc/fix-formating
fix pod formatting (fixes #24)
2023-12-05 20:09:42 +01:00
T.v.Dein
b4f53d2dd6 Merge pull request #26 from TLINDEN/feature/add-shortcuts
added a couple of command shortcuts
2023-12-05 20:06:05 +01:00
ec4d86f727 added a couple of shortcuts 2023-12-05 20:01:32 +01:00
4c6caa7114 fix pod formatting (fixes #24) 2023-12-04 18:03:58 +01:00
T.v.Dein
252e7eb8d9 Feature/edit stack command (#23)
* add edit feature
* bump version
2023-12-04 13:53:18 +01:00
T.v.Dein
416c163d94 Merge pull request #22 from TLINDEN/doc/improve
add converter docs
2023-11-14 20:01:19 +01:00
d93fbe33dc add converter docs 2023-11-14 20:00:09 +01:00
T.v.Dein
59241932e0 Feature/add converters and bitwise ops (#20)
* added:

- converters
- bitwise operators
- hex input and output support
2023-11-13 15:51:07 +01:00
T.v.Dein
127483eea1 Merge pull request #19 from TLINDEN/revert/exists
revert exists(), unused
2023-11-12 20:41:58 +01:00
4846691c46 revert exists(), unused 2023-11-12 20:41:06 +01:00
T.v.Dein
2f56761bf1 Feature/commands (#18)
* re-organized command structure
* added 'dup' command
2023-11-12 20:29:10 +01:00
T.v.Dein
0782b0920b Merge pull request #17 from TLINDEN/feature/add-disable-swiches
added no versions of batch debug and showstack toggles
2023-11-10 15:44:07 +01:00
40c4cf0e45 added no versions of batch debug and showstack toggles 2023-11-10 15:38:05 +01:00
T.v.Dein
b13fbc63e3 Merge pull request #16 from TLINDEN/internal/add-lua-tests
Internal/add lua tests
2023-11-09 18:49:32 +01:00
bacbfcc517 added debug output to Backup() 2023-11-09 18:47:31 +01:00
b91e024569 added more tests 2023-11-09 18:34:38 +01:00
a6f8a0fdbe renamed luafunc() 2023-11-09 18:34:29 +01:00
7b656c492a fix reverse and backup&restore 2023-11-09 18:34:07 +01:00
7d0443ce4b bump version 2023-11-09 18:34:00 +01:00
T.v.Dein
a964a99f3d Fix/lua no funcs known (#15)
* lua fixes:

- fix lua function calling, didn't work in the last
releases (regression)
- add lua funcs which don't modify the stack (for converters etc)
- added better lua examples
2023-11-08 19:03:37 +01:00
T.v.Dein
31a0ddd547 add variable support, implements #10 (#14) 2023-11-08 14:47:15 +01:00
T.v.Dein
fa5f8dcb3b suppress intermediate results unless -i, addresses #11, fix man (#13) 2023-11-08 14:43:34 +01:00
T.v.Dein
64e66e9d7b bump version (#9) 2023-11-07 14:19:47 +01:00
T.v.Dein
23a4d87514 add screencast (#8) 2023-11-07 14:19:32 +01:00
T.v.Dein
2ce8cc7a7e add comments support (#7) 2023-11-07 14:18:46 +01:00
T.v.Dein
e963a770a7 bump version (#6) 2023-11-06 20:14:21 +01:00
T.v.Dein
ad2d9d98d6 Doc/improve (#4)
* add interactiveness to features, add keybindings to doc
* added commands
2023-11-06 20:12:31 +01:00
T.v.Dein
bb49cb7626 Feature/add show stack (#5)
* add -s flag and show command to display the last 5 entries
2023-11-06 20:12:07 +01:00
T.v.Dein
9441be35ef added video explanation (#1)
* added video explanation
2023-11-06 16:12:33 +01:00
T.v.Dein
dac5c0967a added swap stack command, bump version (#2) 2023-11-06 16:11:12 +01:00
T.v.Dein
b5430403fd Internal/add gh actions and tests (#3)
* add gh actions and templates
* add show-versions in Makefile
* force go 1.20
* added test facilities
2023-11-06 16:09:56 +01:00
48 changed files with 2421 additions and 488 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[bug-report]"
labels: bug
assignees: TLINDEN
---
**Description**
<!-- Please provide a clear and concise description of the issue: -->
**Steps To Reproduce**
<!-- Please detail the steps to reproduce the behavior: -->
**Expected behavior**
<!-- What do you expected to happen instead? -->
**Version information**
<!--
Please provide as much version information as possible:
- if you have just installed a binary, provide the output of: rpn -v
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->
**Additional informations**

View File

@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest a feature
title: "[feature-request]"
labels: feature-request
assignees: TLINDEN
---
**Describtion**
<!-- Please provide a clear and concise description of the feature you desire: -->
**Version information**
<!--
Just in case the feature is already present, please provide as
much version information as possible:
- if you have just installed a binary, provide the output of: rpn -v
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

38
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: build-and-test-rpn
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
version: [1.22.1]
os: [ubuntu-latest, windows-latest, macos-latest]
name: Build
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go ${{ matrix.version }}
uses: actions/setup-go@v5
with:
go-version: '${{ matrix.version }}'
id: go
- name: checkout
uses: actions/checkout@v4
- name: build
run: make buildlocal
- name: test
run: make test
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
skip-cache: true

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
releases releases
rpn rpn
rpn.1 rpn.1
coverage.out

View File

@@ -39,10 +39,6 @@ ifdef HAVE_POD
echo "var manpage = \`" >> $*.go echo "var manpage = \`" >> $*.go
pod2text $*.pod >> $*.go pod2text $*.pod >> $*.go
echo "\`" >> $*.go echo "\`" >> $*.go
echo "var usage = \`" >> $*.go
awk '/SYNOPS/{f=1;next} /DESCR/{f=0} f' $*.pod | sed 's/^ //' >> $*.go
echo "\`" >> $*.go
endif endif
buildlocal: buildlocal:
@@ -55,15 +51,25 @@ install: buildlocal
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/ install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean: clean:
rm -rf $(tool) coverage.out rm -rf $(tool) coverage.out testdata
test: test: clean
go test -v ./... go test ./... $(ARGS)
bash t/test.sh
testfuzzy: clean
go test -fuzz ./... $(ARGS)
testlint: test lint
lint:
golangci-lint run
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,godox,dupword,forcetypeassert,goerr113,gomnd
singletest: singletest:
@echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib" @echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) github.com/tlinden/rpn/$(MOD) go test -run $(TEST) $(ARGS)
cover-report: cover-report:
go test ./... -cover -coverprofile=coverage.out go test ./... -cover -coverprofile=coverage.out
@@ -77,3 +83,15 @@ buildall:
release: buildall release: buildall
gh release create v$(VERSION) --generate-notes releases/* gh release create v$(VERSION) --generate-notes releases/*
show-versions: buildlocal
@echo "### rpn version:"
@./rpn -v
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod

View File

@@ -1,4 +1,8 @@
## Reverse Polish Notation Calculator for the commandline ## Programmable command-line calculator using reverse polish notation
[![Actions](https://github.com/tlinden/rpnc/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/rpnc/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/rpnc/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/rpnc)](https://goreportcard.com/report/github.com/tlinden/rpnc)
This is a small commandline calculator which takes its input in This is a small commandline calculator which takes its input in
[reverse polish notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation) [reverse polish notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation)
@@ -11,10 +15,18 @@ Features:
- various stack manipulation commands - various stack manipulation commands
- basic math operators - basic math operators
- advanced math functions (not yet complete) - advanced math functions (not yet complete)
- provides interactive repl
- can be used on the commandline - can be used on the commandline
- can calculate data in batch mode (also from STDIN) - can calculate data in batch mode (also from STDIN)
- extensible with custom LUA functions - extensible with custom LUA functions
- provides interactive repl
- completion
- history
- comments (comment character is `#`)
- variables
## Demo
![asciicast](demo/demo.gif)
## Working principle ## Working principle
@@ -65,6 +77,8 @@ DEBUG(012): push to stack: 200.00
= 200 = 200
``` ```
For a very good explanation how reverse polish notation and the stack works [watch this video by Prof. Brailsford](https://youtu.be/7ha78yWRDlE?si=9MCp59jAAG8fXavP)
## Usage ## Usage
Basically you enter numbers followed by an operator or a Basically you enter numbers followed by an operator or a
@@ -211,10 +225,15 @@ the `register()` function to register your functions to the
calculator. This function takes these parameters: calculator. This function takes these parameters:
- function name - function name
- number of arguments expected (1,2 or -1 allowed), -1 means batch - number of arguments expected (see below)
mode
- help text - help text
Number of expected arguments can be:
- 0: expect 1 argument but do NOT modify the stack
- 1-n: do a singular calculation
- -1: batch mode work with all numbers on the stack
Please [refer to the lua language Please [refer to the lua language
reference](https://www.lua.org/manual/5.4/) for more details about reference](https://www.lua.org/manual/5.4/) for more details about
LUA. LUA.

578
calc.go
View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2023 Thomas von Dein Copyright © 2023-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -21,8 +21,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"math" "math"
"os"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
@@ -33,35 +33,43 @@ type Calc struct {
debug bool debug bool
batch bool batch bool
stdin bool stdin bool
showstack bool
intermediate bool
notdone bool // set to true as long as there are items left in the eval loop
precision int
stack *Stack stack *Stack
history []string history []string
completer readline.AutoCompleter completer readline.AutoCompleter
interpreter *Interpreter interpreter *Interpreter
Space *regexp.Regexp Space *regexp.Regexp
Comment *regexp.Regexp
Register *regexp.Regexp
Constants []string Constants []string
LuaFunctions []string LuaFunctions []string
Funcalls Funcalls Funcalls Funcalls
BatchFuncalls Funcalls BatchFuncalls Funcalls
// different kinds of commands, displays nicer in help output
StackCommands Commands
SettingsCommands Commands
ShowCommands Commands
Commands Commands
Vars map[string]float64
} }
// help for lua functions will be added dynamically // help for lua functions will be added dynamically
const Help string = `Available commands: const Help string = `
batch toggle batch mode
debug toggle debug output
dump display the stack contents
clear clear the whole stack
shift remove the last element of the stack
history display calculation history
help|? show this message
quit|exit|c-d|c-c exit program
Operators: Operators:
basic operators: + - x * / ^ (* is an alias of x) basic operators: + - x * / ^ (* is an alias of x)
Bitwise operators: and or xor < (left shift) > (right shift)
Percent functions: Percent functions:
% percent % percent
%- substract percent %- subtract percent
%+ add percent %+ add percent
Math functions (see https://pkg.go.dev/math): Math functions (see https://pkg.go.dev/math):
@@ -75,13 +83,18 @@ sum sum of all values (alias: +)
max max of all values max max of all values
min min of all values min min of all values
mean mean of all values (alias: avg) mean mean of all values (alias: avg)
median median of all values` median median of all values
Register variables:
>NAME Put last stack element into variable NAME
<NAME Retrieve variable NAME and put onto stack`
// commands, constants and operators, defined here to feed completion // commands, constants and operators, defined here to feed completion
// and our mode switch in Eval() dynamically // and our mode switch in Eval() dynamically
const ( const (
Commands string = `dump reverse debug undebug clear batch shift undo help history manual exit quit` Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E`
Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E` Precision int = 2
ShowStackLen int = 5
) )
// That way we can add custom functions to completion // That way we can add custom functions to completion
@@ -93,7 +106,6 @@ func GetCompleteCustomFunctions() func(string) []string {
completions = append(completions, luafunc) completions = append(completions, luafunc)
} }
completions = append(completions, strings.Split(Commands, " ")...)
completions = append(completions, strings.Split(Constants, " ")...) completions = append(completions, strings.Split(Constants, " ")...)
return completions return completions
@@ -112,38 +124,66 @@ func (c *Calc) GetCompleteCustomFuncalls() func(string) []string {
completions = append(completions, function) completions = append(completions, function)
} }
for command := range c.SettingsCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.ShowCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.StackCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.Commands {
if len(command) > 1 {
completions = append(completions, command)
}
}
return completions return completions
} }
} }
func NewCalc() *Calc { func NewCalc() *Calc {
c := Calc{stack: NewStack(), debug: false} calc := Calc{stack: NewStack(), debug: false, precision: Precision}
c.Funcalls = DefineFunctions() calc.Funcalls = DefineFunctions()
c.BatchFuncalls = DefineBatchFunctions() calc.BatchFuncalls = DefineBatchFunctions()
calc.Vars = map[string]float64{}
c.completer = readline.NewPrefixCompleter( calc.completer = readline.NewPrefixCompleter(
// custom lua functions // custom lua functions
readline.PcItemDynamic(GetCompleteCustomFunctions()), readline.PcItemDynamic(GetCompleteCustomFunctions()),
readline.PcItemDynamic(c.GetCompleteCustomFuncalls()), readline.PcItemDynamic(calc.GetCompleteCustomFuncalls()),
) )
c.Space = regexp.MustCompile(`\s+`) calc.Space = regexp.MustCompile(`\s+`)
calc.Comment = regexp.MustCompile(`#.*`) // ignore everything after #
calc.Register = regexp.MustCompile(`^([<>])([A-Z][A-Z0-9]*)`)
// pre-calculate mode switching arrays // pre-calculate mode switching arrays
c.Constants = strings.Split(Constants, " ") calc.Constants = strings.Split(Constants, " ")
calc.SetCommands()
return &calc
}
// setup the interpreter, called from main(), import lua functions
func (c *Calc) SetInt(interpreter *Interpreter) {
c.interpreter = interpreter
for name := range LuaFuncs { for name := range LuaFuncs {
c.LuaFunctions = append(c.LuaFunctions, name) c.LuaFunctions = append(c.LuaFunctions, name)
} }
return &c
}
// setup the interpreter, called from main()
func (c *Calc) SetInt(I *Interpreter) {
c.interpreter = I
} }
func (c *Calc) ToggleDebug() { func (c *Calc) ToggleDebug() {
@@ -161,133 +201,185 @@ func (c *Calc) ToggleStdin() {
c.stdin = !c.stdin c.stdin = !c.stdin
} }
func (c *Calc) ToggleShow() {
c.showstack = !c.showstack
}
func (c *Calc) Prompt() string { func (c *Calc) Prompt() string {
p := "\033[31m»\033[0m " prompt := "\033[31m»\033[0m "
b := "" batch := ""
if c.batch { if c.batch {
b = "->batch" batch = "->batch"
}
d := ""
v := ""
if c.debug {
d = "->debug"
v = fmt.Sprintf("/rev%d", c.stack.rev)
} }
return fmt.Sprintf("rpn%s%s [%d%s]%s", b, d, c.stack.Len(), v, p) debug := ""
revision := ""
if c.debug {
debug = "->debug"
revision = fmt.Sprintf("/rev%d", c.stack.rev)
}
return fmt.Sprintf("rpn%s%s [%d%s]%s", batch, debug, c.stack.Len(), revision, prompt)
} }
// the actual work horse, evaluate a line of calc command[s] // the actual work horse, evaluate a line of calc command[s]
func (c *Calc) Eval(line string) { func (c *Calc) Eval(line string) error {
line = strings.TrimSpace(line) // remove surrounding whitespace and comments, if any
line = strings.TrimSpace(c.Comment.ReplaceAllString(line, ""))
if line == "" { if line == "" {
return return nil
} }
for _, item := range c.Space.Split(line, -1) { items := c.Space.Split(line, -1)
num, err := strconv.ParseFloat(item, 64)
if err == nil { for pos, item := range items {
c.stack.Backup() if pos+1 < len(items) {
c.stack.Push(num) c.notdone = true
} else { } else {
/* c.notdone = false
if contains(c.MathFunctions, item) { }
// go builtin math function, if implemented
c.mathfunc(item)
continue
}
if contains(c.BatchFunctions, item) { if err := c.EvalItem(item); err != nil {
// math functions only supported in batch mode like max or mean return err
c.batchfunc(item)
continue
}
*/
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
continue
}
if _, ok := c.Funcalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
}
continue
}
if c.batch {
if _, ok := c.BatchFuncalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
}
continue
}
} else {
if _, ok := c.BatchFuncalls[item]; ok {
fmt.Println("only supported in batch mode")
continue
}
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.luafunc(item)
continue
}
// management commands
switch item {
case "?":
fallthrough
case "help":
fmt.Println(Help)
fmt.Println("Lua functions:")
for name, function := range LuaFuncs {
fmt.Printf("%-20s %s\n", name, function.help)
}
case "dump":
c.stack.Dump()
case "debug":
c.ToggleDebug()
case "undebug":
c.debug = false
case "batch":
c.ToggleBatch()
case "clear":
c.stack.Backup()
c.stack.Clear()
case "shift":
c.stack.Backup()
c.stack.Shift()
case "reverse":
c.stack.Backup()
c.stack.Reverse()
case "undo":
c.stack.Restore()
case "history":
for _, entry := range c.history {
fmt.Println(entry)
}
case "exit":
fallthrough
case "quit":
os.Exit(0)
case "manual":
man()
default:
fmt.Println("unknown command or operator!")
}
} }
} }
if c.showstack && !c.stdin {
dots := ""
if c.stack.Len() > ShowStackLen {
dots = "... "
}
last := c.stack.Last(ShowStackLen)
fmt.Printf("stack: %s%s\n", dots, list2str(last))
}
return nil
}
func (c *Calc) EvalItem(item string) error {
num, err := strconv.ParseFloat(item, 64)
if err == nil {
c.stack.Backup()
c.stack.Push(num)
return nil
}
// try time
var hour, min int
_, err = fmt.Sscanf(item, "%d:%d", &hour, &min)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(hour) + float64(min)/60)
return nil
}
// try hex
var i int
_, err = fmt.Sscanf(item, "0x%x", &i)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(i))
return nil
}
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
return nil
}
if exists(c.Funcalls, item) {
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
}
c.Result()
return nil
}
if exists(c.BatchFuncalls, item) {
if !c.batch {
return Error("only supported in batch mode")
}
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
}
c.Result()
return nil
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.EvalLuaFunction(item)
return nil
}
regmatches := c.Register.FindStringSubmatch(item)
if len(regmatches) == 3 {
switch regmatches[1] {
case ">":
c.PutVar(regmatches[2])
case "<":
c.GetVar(regmatches[2])
}
return nil
}
// internal commands
// FIXME: propagate errors
if exists(c.Commands, item) {
c.Commands[item].Func(c)
return nil
}
if exists(c.ShowCommands, item) {
c.ShowCommands[item].Func(c)
return nil
}
if exists(c.StackCommands, item) {
c.StackCommands[item].Func(c)
return nil
}
if exists(c.SettingsCommands, item) {
c.SettingsCommands[item].Func(c)
return nil
}
switch item {
case "?":
fallthrough
case "help":
c.PrintHelp()
default:
return Error("unknown command or operator")
}
return nil
} }
// Execute a math function, check if it is defined just in case // Execute a math function, check if it is defined just in case
@@ -300,10 +392,11 @@ func (c *Calc) DoFuncall(funcname string) error {
} }
if function == nil { if function == nil {
panic("function not defined but in completion list") return Error("function not defined but in completion list")
} }
var args Numbers var args Numbers
batch := false batch := false
if function.Expectargs == -1 { if function.Expectargs == -1 {
@@ -325,13 +418,17 @@ func (c *Calc) DoFuncall(funcname string) error {
// the actual lambda call, so to say. We provide a slice of // the actual lambda call, so to say. We provide a slice of
// the requested size, fetched from the stack (but not popped // the requested size, fetched from the stack (but not popped
// yet!) // yet!)
R := function.Func(args) funcresult := function.Func(args)
if R.Err != nil { if funcresult.Err != nil {
// leave the stack untouched in case of any error // leave the stack untouched in case of any error
return R.Err return funcresult.Err
} }
// don't forget to backup!
c.stack.Backup()
// "pop"
if batch { if batch {
// get rid of stack // get rid of stack
c.stack.Clear() c.stack.Clear()
@@ -341,10 +438,11 @@ func (c *Calc) DoFuncall(funcname string) error {
} }
// save result // save result
c.stack.Push(R.Res) c.stack.Push(funcresult.Res)
// thanks a lot // thanks a lot
c.SetHistory(funcname, args, R.Res) c.SetHistory(funcname, args, funcresult.Res)
return nil return nil
} }
@@ -361,11 +459,25 @@ func (c *Calc) History(format string, args ...any) {
// print the result // print the result
func (c *Calc) Result() float64 { func (c *Calc) Result() float64 {
if !c.stdin { // we only print the result if it's either a final result or
fmt.Print("= ") // (if it is intermediate) if -i has been given
} if c.intermediate || !c.notdone {
// only needed in repl
if !c.stdin {
fmt.Print("= ")
}
fmt.Println(c.stack.Last()[0]) result := c.stack.Last()[0]
truncated := math.Trunc(result)
precision := c.precision
if result == truncated {
precision = 0
}
format := fmt.Sprintf("%%.%df\n", precision)
fmt.Printf(format, result)
}
return c.stack.Last()[0] return c.stack.Last()[0]
} }
@@ -376,87 +488,139 @@ func (c *Calc) Debug(msg string) {
} }
} }
// do simple calculations func (c *Calc) EvalLuaFunction(funcname string) {
func (c *Calc) simple(op byte) {
c.stack.Backup()
for c.stack.Len() > 1 {
b := c.stack.Pop()
a := c.stack.Pop()
var x float64
c.Debug(fmt.Sprintf("evaluating: %.2f %c %.2f", a, op, b))
switch op {
case '+':
x = a + b
case '-':
x = a - b
case 'x':
fallthrough // alias for *
case '*':
x = a * b
case '/':
if b == 0 {
fmt.Println("error: division by null!")
return
}
x = a / b
case '^':
x = math.Pow(a, b)
default:
panic("invalid operator!")
}
c.stack.Push(x)
c.History("%f %c %f = %f", a, op, b, x)
if !c.batch {
break
}
}
c.Result()
}
func (c *Calc) luafunc(funcname string) {
// called from calc loop // called from calc loop
var x float64 var luaresult float64
var err error var err error
switch c.interpreter.FuncNumArgs(funcname) { switch c.interpreter.FuncNumArgs(funcname) {
case 0:
fallthrough
case 1: case 1:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last()) luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last())
case 2: case 2:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last(2)) luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last(2))
case -1: case -1:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.All()) luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.All())
default: default:
x, err = 0, errors.New("invalid number of argument requested") luaresult, err = 0, errors.New("invalid number of argument requested")
} }
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
} }
c.stack.Backup() c.stack.Backup()
dopush := true
switch c.interpreter.FuncNumArgs(funcname) { switch c.interpreter.FuncNumArgs(funcname) {
case 0:
a := c.stack.Last()
if len(a) == 1 {
c.History("%s(%f) = %f", funcname, a, luaresult)
}
dopush = false
case 1: case 1:
a := c.stack.Pop() a := c.stack.Pop()
c.History("%s(%f) = %f", funcname, a, x) c.History("%s(%f) = %f", funcname, a, luaresult)
case 2: case 2:
a := c.stack.Pop() a := c.stack.Pop()
b := c.stack.Pop() b := c.stack.Pop()
c.History("%s(%f,%f) = %f", funcname, a, b, x) c.History("%s(%f,%f) = %f", funcname, a, b, luaresult)
case -1: case -1:
c.stack.Clear() c.stack.Clear()
c.History("%s(*) = %f", funcname, x) c.History("%s(*) = %f", funcname, luaresult)
} }
c.stack.Push(x) if dopush {
c.stack.Push(luaresult)
}
c.Result() c.Result()
} }
func (c *Calc) PutVar(name string) {
last := c.stack.Last()
if len(last) == 1 {
c.Debug(fmt.Sprintf("register %.2f in %s", last[0], name))
c.Vars[name] = last[0]
} else {
fmt.Println("empty stack")
}
}
func (c *Calc) GetVar(name string) {
if exists(c.Vars, name) {
c.Debug(fmt.Sprintf("retrieve %.2f from %s", c.Vars[name], name))
c.stack.Backup()
c.stack.Push(c.Vars[name])
} else {
fmt.Println("variable doesn't exist")
}
}
func sortcommands(hash Commands) []string {
keys := make([]string, 0, len(hash))
for key := range hash {
if len(key) > 1 {
keys = append(keys, key)
}
}
sort.Strings(keys)
return keys
}
func (c *Calc) PrintHelp() {
fmt.Println("Available configuration commands:")
for _, name := range sortcommands(c.SettingsCommands) {
fmt.Printf("%-20s %s\n", name, c.SettingsCommands[name].Help)
}
fmt.Println()
fmt.Println("Available show commands:")
for _, name := range sortcommands(c.ShowCommands) {
fmt.Printf("%-20s %s\n", name, c.ShowCommands[name].Help)
}
fmt.Println()
fmt.Println("Available stack manipulation commands:")
for _, name := range sortcommands(c.StackCommands) {
fmt.Printf("%-20s %s\n", name, c.StackCommands[name].Help)
}
fmt.Println()
fmt.Println("Other commands:")
for _, name := range sortcommands(c.Commands) {
fmt.Printf("%-20s %s\n", name, c.Commands[name].Help)
}
fmt.Println()
fmt.Println(Help)
// append lua functions, if any
if len(LuaFuncs) > 0 {
fmt.Println("Lua functions:")
for name, function := range LuaFuncs {
fmt.Printf("%-20s %s\n", name, function.help)
}
}
}

418
calc_test.go Normal file
View File

@@ -0,0 +1,418 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"strconv"
"strings"
"testing"
lua "github.com/yuin/gopher-lua"
)
func TestCommentsAndWhitespace(t *testing.T) {
calc := NewCalc()
var tests = []struct {
name string
cmd []string
exp float64 // last element of the stack
}{
{
name: "whitespace prefix",
cmd: []string{" 5"},
exp: 5.0,
},
{
name: "whitespace postfix",
cmd: []string{"5 "},
exp: 5.0,
},
{
name: "whitespace both",
cmd: []string{" 5 "},
exp: 5.0,
},
{
name: "comment line w/ spaces",
cmd: []string{"5", " # 19"},
exp: 5.0,
},
{
name: "comment line w/o spaces",
cmd: []string{"5", `#19`},
exp: 5.0,
},
{
name: "inline comment w/ spaces",
cmd: []string{"5 # 19"},
exp: 5.0,
},
{
name: "inline comment w/o spaces",
cmd: []string{"5#19"},
exp: 5.0,
},
}
for _, test := range tests {
testname := fmt.Sprintf("%s .(expect %.2f)",
test.name, test.exp)
t.Run(testname, func(t *testing.T) {
for _, line := range test.cmd {
if err := calc.Eval(line); err != nil {
t.Error(err.Error())
}
}
got := calc.stack.Last()
if len(got) > 0 {
if got[0] != test.exp {
t.Errorf("parsing failed:\n+++ got: %f\n--- want: %f",
got, test.exp)
}
}
if calc.stack.Len() != 1 {
t.Errorf("invalid stack size:\n+++ got: %d\n--- want: 1",
calc.stack.Len())
}
})
calc.stack.Clear()
}
}
func TestCalc(t *testing.T) {
calc := NewCalc()
var tests = []struct {
name string
cmd string
exp float64
batch bool
}{
// ops
{
name: "plus",
cmd: `15 15 +`,
exp: 30,
},
{
name: "power",
cmd: `4 2 ^`,
exp: 16,
},
{
name: "minus",
cmd: `100 50 -`,
exp: 50,
},
{
name: "multi",
cmd: `4 4 x`,
exp: 16,
},
{
name: "divide",
cmd: `10 2 /`,
exp: 5,
},
{
name: "percent",
cmd: `400 20 %`,
exp: 80,
},
{
name: "percent-minus",
cmd: `400 20 %-`,
exp: 320,
},
{
name: "percent-plus",
cmd: `400 20 %+`,
exp: 480,
},
// math tests
{
name: "mod",
cmd: `9 2 mod`,
exp: 1,
},
{
name: "sqrt",
cmd: `16 sqrt`,
exp: 4,
},
{
name: "ceil",
cmd: `15.5 ceil`,
exp: 16,
},
{
name: "dim",
cmd: `6 4 dim`,
exp: 2,
},
// constants tests
{
name: "pitimes2",
cmd: `Pi 2 *`,
exp: 6.283185307179586,
},
{
name: "pi+sqrt2",
cmd: `Pi Sqrt2 +`,
exp: 4.555806215962888,
},
// batch tests
{
name: "batch-sum",
cmd: `2 2 2 2 sum`,
exp: 8,
batch: true,
},
{
name: "batch-median",
cmd: `1 2 3 4 5 median`,
exp: 3,
batch: true,
},
{
name: "batch-mean",
cmd: `2 2 8 2 2 mean`,
exp: 3.2,
batch: true,
},
{
name: "batch-min",
cmd: `1 2 3 4 5 min`,
exp: 1,
batch: true,
},
{
name: "batch-max",
cmd: `1 2 3 4 5 max`,
exp: 5,
batch: true,
},
// stack tests
{
name: "use-vars",
cmd: `10 >TEN clear 5 <TEN *`,
exp: 50,
},
{
name: "reverse",
cmd: `100 500 reverse -`,
exp: 400,
},
{
name: "swap",
cmd: `2 16 swap /`,
exp: 8,
},
{
name: "clear batch",
cmd: "1 1 1 1 1 clear 1 1 sum",
exp: 2,
batch: true,
},
{
name: "undo",
cmd: `4 4 + undo *`,
exp: 16,
},
// bit tests
{
name: "bit and",
cmd: `1 3 and`,
exp: 1,
},
{
name: "bit or",
cmd: `1 3 or`,
exp: 3,
},
{
name: "bit xor",
cmd: `1 3 xor`,
exp: 2,
},
// converters
{
name: "inch-to-cm",
cmd: `111 inch-to-cm`,
exp: 281.94,
},
{
name: "gallons-to-liters",
cmd: `111 gallons-to-liters`,
exp: 420.135,
},
{
name: "meters-to-yards",
cmd: `111 meters-to-yards`,
exp: 1.2139107611548556,
},
{
name: "miles-to-kilometers",
cmd: `111 miles-to-kilometers`,
exp: 178.599,
},
}
for _, test := range tests {
testname := fmt.Sprintf("cmd-%s-expect-%.2f",
test.name, test.exp)
t.Run(testname, func(t *testing.T) {
calc.batch = test.batch
if err := calc.Eval(test.cmd); err != nil {
t.Error(err.Error())
}
got := calc.Result()
calc.stack.Clear()
if got != test.exp {
t.Errorf("calc failed:\n+++ got: %f\n--- want: %f",
got, test.exp)
}
})
}
}
func TestCalcLua(t *testing.T) {
var tests = []struct {
function string
stack []float64
exp float64
}{
{
function: "lower",
stack: []float64{5, 6},
exp: 5.0,
},
{
function: "parallelresistance",
stack: []float64{100, 200, 300},
exp: 54.54545454545455,
},
}
calc := NewCalc()
LuaInterpreter = lua.NewState(lua.Options{SkipOpenLibs: true})
defer LuaInterpreter.Close()
luarunner := NewInterpreter("example.lua", false)
luarunner.InitLua()
calc.SetInt(luarunner)
for _, test := range tests {
testname := fmt.Sprintf("lua-%s", test.function)
t.Run(testname, func(t *testing.T) {
calc.stack.Clear()
for _, item := range test.stack {
calc.stack.Push(item)
}
calc.EvalLuaFunction(test.function)
got := calc.stack.Last()
if calc.stack.Len() != 1 {
t.Errorf("invalid stack size:\n+++ got: %d\n--- want: 1",
calc.stack.Len())
}
if got[0] != test.exp {
t.Errorf("lua function %s failed:\n+++ got: %f\n--- want: %f",
test.function, got, test.exp)
}
})
}
}
func FuzzEval(f *testing.F) {
legal := []string{
"dump",
"showstack",
"help",
"Pi 31 *",
"SqrtE Pi /",
"55.5 yards-to-meters",
"2 4 +",
"7 8 batch sum",
"7 8 %-",
"7 8 clear",
"7 8 /",
"b",
"#444",
"<X",
}
for _, item := range legal {
f.Add(item)
}
calc := NewCalc()
var hexnum, hour, min int
f.Fuzz(func(t *testing.T, line string) {
t.Logf("Stack:\n%v\n", calc.stack.All())
if err := calc.EvalItem(line); err == nil {
t.Logf("given: <%s>", line)
// not corpus and empty?
if !contains(legal, line) && len(line) > 0 {
item := strings.TrimSpace(calc.Comment.ReplaceAllString(line, ""))
_, hexerr := fmt.Sscanf(item, "0x%x", &hexnum)
_, timeerr := fmt.Sscanf(item, "%d:%d", &hour, &min)
// no comment?
if len(item) > 0 {
// no known command or function?
if _, err := strconv.ParseFloat(item, 64); err != nil {
if !contains(calc.Constants, item) &&
!exists(calc.Funcalls, item) &&
!exists(calc.BatchFuncalls, item) &&
!contains(calc.LuaFunctions, item) &&
!exists(calc.Commands, item) &&
!exists(calc.ShowCommands, item) &&
!exists(calc.SettingsCommands, item) &&
!exists(calc.StackCommands, item) &&
!calc.Register.MatchString(item) &&
item != "?" && item != "help" &&
hexerr != nil &&
timeerr != nil {
t.Errorf("Fuzzy input accepted: <%s>", line)
}
}
}
}
}
})
}

350
command.go Normal file
View File

@@ -0,0 +1,350 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
)
type CommandFunction func(*Calc)
type Command struct {
Help string
Func CommandFunction
}
type Commands map[string]*Command
func NewCommand(help string, function CommandFunction) *Command {
return &Command{
Help: help,
Func: function,
}
}
func (c *Calc) SetSettingsCommands() Commands {
return Commands{
// Toggles
"debug": NewCommand(
"toggle debugging",
func(c *Calc) {
c.ToggleDebug()
},
),
"nodebug": NewCommand(
"disable debugging",
func(c *Calc) {
c.debug = false
c.stack.debug = false
},
),
"batch": NewCommand(
"toggle batch mode",
func(c *Calc) {
c.ToggleBatch()
},
),
"nobatch": NewCommand(
"disable batch mode",
func(c *Calc) {
c.batch = false
},
),
"showstack": NewCommand(
"toggle show last 5 items of the stack",
func(c *Calc) {
c.ToggleShow()
},
),
"noshowstack": NewCommand(
"disable display of the stack",
func(c *Calc) {
c.showstack = false
},
),
}
}
func (c *Calc) SetShowCommands() Commands {
return Commands{
// Display commands
"dump": NewCommand(
"display the stack contents",
func(c *Calc) {
c.stack.Dump()
},
),
"history": NewCommand(
"display calculation history",
func(c *Calc) {
for _, entry := range c.history {
fmt.Println(entry)
}
},
),
"vars": NewCommand(
"show list of variables",
func(c *Calc) {
if len(c.Vars) > 0 {
fmt.Printf("%-20s %s\n", "VARIABLE", "VALUE")
for k, v := range c.Vars {
fmt.Printf("%-20s -> %.2f\n", k, v)
}
} else {
fmt.Println("no vars registered")
}
},
),
"hex": NewCommand(
"show last stack item in hex form (converted to int)",
func(c *Calc) {
if c.stack.Len() > 0 {
fmt.Printf("0x%x\n", int(c.stack.Last()[0]))
}
},
),
}
}
func (c *Calc) SetStackCommands() Commands {
return Commands{
"clear": NewCommand(
"clear the whole stack",
func(c *Calc) {
c.stack.Backup()
c.stack.Clear()
},
),
"shift": NewCommand(
"remove the last element of the stack",
func(c *Calc) {
c.stack.Backup()
c.stack.Shift()
},
),
"reverse": NewCommand(
"reverse the stack elements",
func(c *Calc) {
c.stack.Backup()
c.stack.Reverse()
},
),
"swap": NewCommand(
"exchange the last two elements",
CommandSwap,
),
"undo": NewCommand(
"undo last operation",
func(c *Calc) {
c.stack.Restore()
},
),
"dup": NewCommand(
"duplicate last stack item",
CommandDup,
),
"edit": NewCommand(
"edit the stack interactively",
CommandEdit,
),
}
}
// define all management (that is: non calculation) commands
func (c *Calc) SetCommands() {
c.SettingsCommands = c.SetSettingsCommands()
c.ShowCommands = c.SetShowCommands()
c.StackCommands = c.SetStackCommands()
// general commands
c.Commands = Commands{
"exit": NewCommand(
"exit program",
func(c *Calc) {
os.Exit(0)
},
),
"manual": NewCommand(
"show manual",
func(c *Calc) {
man()
},
),
}
// aliases
c.Commands["quit"] = c.Commands["exit"]
c.SettingsCommands["d"] = c.SettingsCommands["debug"]
c.SettingsCommands["b"] = c.SettingsCommands["batch"]
c.SettingsCommands["s"] = c.SettingsCommands["showstack"]
c.SettingsCommands["togglebatch"] = c.SettingsCommands["batch"]
c.SettingsCommands["toggledebug"] = c.SettingsCommands["debug"]
c.SettingsCommands["toggleshowstack"] = c.SettingsCommands["showstack"]
c.ShowCommands["h"] = c.ShowCommands["history"]
c.ShowCommands["p"] = c.ShowCommands["dump"]
c.ShowCommands["v"] = c.ShowCommands["vars"]
c.StackCommands["c"] = c.StackCommands["clear"]
c.StackCommands["u"] = c.StackCommands["undo"]
}
// added to the command map:
func CommandSwap(c *Calc) {
if c.stack.Len() < 2 {
fmt.Println("stack too small, can't swap")
} else {
c.stack.Backup()
c.stack.Swap()
}
}
func CommandDup(c *Calc) {
item := c.stack.Last()
if len(item) == 1 {
c.stack.Backup()
c.stack.Push(item[0])
} else {
fmt.Println("stack empty")
}
}
func CommandEdit(calc *Calc) {
if calc.stack.Len() == 0 {
fmt.Println("empty stack")
return
}
calc.stack.Backup()
// put the stack contents into a tmp file
tmp, err := os.CreateTemp("", "stack")
if err != nil {
fmt.Println(err)
return
}
defer os.Remove(tmp.Name())
comment := `# add or remove numbers as you wish.
# each number must be on its own line.
# numbers must be floating point formatted.
`
_, err = tmp.WriteString(comment)
if err != nil {
fmt.Println(err)
return
}
for _, item := range calc.stack.All() {
_, err = fmt.Fprintf(tmp, "%f\n", item)
if err != nil {
fmt.Println(err)
return
}
}
tmp.Close()
// determine which editor to use
editor := "vi"
enveditor, present := os.LookupEnv("EDITOR")
if present {
if editor != "" {
if _, err := os.Stat(editor); err == nil {
editor = enveditor
}
}
}
// execute editor with our tmp file containing current stack
cmd := exec.Command(editor, tmp.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
fmt.Println("could not run editor command: ", err)
return
}
// read the file back in
modified, err := os.Open(tmp.Name())
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer modified.Close()
// reset the stack
calc.stack.Clear()
// and put the new contents (if legit) back onto the stack
scanner := bufio.NewScanner(modified)
for scanner.Scan() {
line := strings.TrimSpace(calc.Comment.ReplaceAllString(scanner.Text(), ""))
if line == "" {
continue
}
num, err := strconv.ParseFloat(line, 64)
if err != nil {
fmt.Printf("%s is not a floating point number!\n", line)
continue
}
calc.stack.Push(num)
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading from file:", err)
}
}

7
demo/Makefile Normal file
View File

@@ -0,0 +1,7 @@
#
all:
asciinema rec --cols 80 --row 25 -c "env - PS1='> ' PATH=..:$PATH /bin/bash --norc --noprofile" --overwrite demo.cast
agg demo.cast demo.gif

3
demo/demo.cast Normal file
View File

@@ -0,0 +1,3 @@
{"version": 2, "width": 80, "height": 25, "timestamp": 1699358215, "env": {"SHELL": "/bin/bash", "TERM": "tmux-256color"}}
[0.005628, "o", "> "]
[0.941195, "o", "exit\r\n"]

BIN
demo/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

18
demo/sessions.txt Normal file
View File

@@ -0,0 +1,18 @@
# demo sessions, used to create an interactive rpn recording
# part 1: interactive
# put some numbers onto the stack
2 4 6
8
10
# take a look at the stack
dump
# add the last 2
+
# multiply the result and the previous
*

38
example.lua Normal file
View File

@@ -0,0 +1,38 @@
-- simple function, return the lower number of the two operands
function lower(a,b)
if a < b then
return a
else
return b
end
end
-- calculate parallel resistance. Batch function (registered with -1,
-- see below). Takes a table as parameter.
--
-- Formula: 1/( (1/R1) + (1/R2) + ...)
function parallelresistance(list)
sumres = 0
for i, value in ipairs(list) do
sumres = sumres + 1 / value
end
return 1 / sumres
end
-- converter example
function inch2centimeter(inches)
return inches * 2.54
end
function init()
-- expects 2 args
register("lower", 2, "lower")
-- expects a list of all numbers on the stack, batch mode
register("parallelresistance", -1, "parallel resistance")
-- expects 1 arg, but doesn't pop()
register("inch2centimeter", 0)
end

348
funcs.go
View File

@@ -22,14 +22,14 @@ import (
"math" "math"
) )
type R struct { type Result struct {
Res float64 Res float64
Err error Err error
} }
type Numbers []float64 type Numbers []float64
type Function func(Numbers) R type Function func(Numbers) Result
// every function we are able to call must be of type Funcall, which // every function we are able to call must be of type Funcall, which
// needs to specify how many numbers it expects and the actual go // needs to specify how many numbers it expects and the actual go
@@ -64,359 +64,450 @@ func NewFuncall(function Function, expectargs ...int) *Funcall {
} }
// Convenience function, create new result // Convenience function, create new result
func NewR(n float64, e error) R { func NewResult(n float64, e error) Result {
return R{Res: n, Err: e} return Result{Res: n, Err: e}
} }
// the actual functions, called once during initialization. // the actual functions, called once during initialization.
func DefineFunctions() Funcalls { func DefineFunctions() Funcalls {
f := map[string]*Funcall{ funcmap := map[string]*Funcall{
// simple operators, they all expect 2 args // simple operators, they all expect 2 args
"+": NewFuncall( "+": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(arg[0]+arg[1], nil) return NewResult(arg[0]+arg[1], nil)
}, },
), ),
"-": NewFuncall( "-": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(arg[0]-arg[1], nil) return NewResult(arg[0]-arg[1], nil)
}, },
), ),
"x": NewFuncall( "x": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(arg[0]*arg[1], nil) return NewResult(arg[0]*arg[1], nil)
}, },
), ),
"/": NewFuncall( "/": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
if arg[1] == 0 { if arg[1] == 0 {
return NewR(0, errors.New("division by null")) return NewResult(0, errors.New("division by null"))
} }
return NewR(arg[0]/arg[1], nil) return NewResult(arg[0]/arg[1], nil)
}, },
), ),
"^": NewFuncall( "^": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Pow(arg[0], arg[1]), nil) return NewResult(math.Pow(arg[0], arg[1]), nil)
}, },
), ),
"%": NewFuncall( "%": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR((arg[0]/100)*arg[1], nil) return NewResult((arg[0]/100)*arg[1], nil)
}, },
), ),
"%-": NewFuncall( "%-": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(arg[0]-((arg[0]/100)*arg[1]), nil) return NewResult(arg[0]-((arg[0]/100)*arg[1]), nil)
}, },
), ),
"%+": NewFuncall( "%+": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(arg[0]+((arg[0]/100)*arg[1]), nil) return NewResult(arg[0]+((arg[0]/100)*arg[1]), nil)
}, },
), ),
"mod": NewFuncall( "mod": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Remainder(arg[0], arg[1]), nil) return NewResult(math.Remainder(arg[0], arg[1]), nil)
}, },
), ),
"sqrt": NewFuncall( "sqrt": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Sqrt(arg[0]), nil) return NewResult(math.Sqrt(arg[0]), nil)
}, },
1), 1),
"abs": NewFuncall( "abs": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Abs(arg[0]), nil) return NewResult(math.Abs(arg[0]), nil)
}, },
1), 1),
"acos": NewFuncall( "acos": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Acos(arg[0]), nil) return NewResult(math.Acos(arg[0]), nil)
}, },
1), 1),
"acosh": NewFuncall( "acosh": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Acosh(arg[0]), nil) return NewResult(math.Acosh(arg[0]), nil)
}, },
1), 1),
"asin": NewFuncall( "asin": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Asin(arg[0]), nil) return NewResult(math.Asin(arg[0]), nil)
}, },
1), 1),
"asinh": NewFuncall( "asinh": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Asinh(arg[0]), nil) return NewResult(math.Asinh(arg[0]), nil)
}, },
1), 1),
"atan": NewFuncall( "atan": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Atan(arg[0]), nil) return NewResult(math.Atan(arg[0]), nil)
}, },
1), 1),
"atan2": NewFuncall( "atan2": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Atan2(arg[0], arg[1]), nil) return NewResult(math.Atan2(arg[0], arg[1]), nil)
}, },
2), 2),
"atanh": NewFuncall( "atanh": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Atanh(arg[0]), nil) return NewResult(math.Atanh(arg[0]), nil)
}, },
1), 1),
"cbrt": NewFuncall( "cbrt": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Cbrt(arg[0]), nil) return NewResult(math.Cbrt(arg[0]), nil)
}, },
1), 1),
"ceil": NewFuncall( "ceil": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Ceil(arg[0]), nil) return NewResult(math.Ceil(arg[0]), nil)
}, },
1), 1),
"cos": NewFuncall( "cos": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Cos(arg[0]), nil) return NewResult(math.Cos(arg[0]), nil)
}, },
1), 1),
"cosh": NewFuncall( "cosh": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Cosh(arg[0]), nil) return NewResult(math.Cosh(arg[0]), nil)
}, },
1), 1),
"erf": NewFuncall( "erf": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Erf(arg[0]), nil) return NewResult(math.Erf(arg[0]), nil)
}, },
1), 1),
"erfc": NewFuncall( "erfc": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Erfc(arg[0]), nil) return NewResult(math.Erfc(arg[0]), nil)
}, },
1), 1),
"erfcinv": NewFuncall( "erfcinv": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Erfcinv(arg[0]), nil) return NewResult(math.Erfcinv(arg[0]), nil)
}, },
1), 1),
"erfinv": NewFuncall( "erfinv": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Erfinv(arg[0]), nil) return NewResult(math.Erfinv(arg[0]), nil)
}, },
1), 1),
"exp": NewFuncall( "exp": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Exp(arg[0]), nil) return NewResult(math.Exp(arg[0]), nil)
}, },
1), 1),
"exp2": NewFuncall( "exp2": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Exp2(arg[0]), nil) return NewResult(math.Exp2(arg[0]), nil)
}, },
1), 1),
"expm1": NewFuncall( "expm1": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Expm1(arg[0]), nil) return NewResult(math.Expm1(arg[0]), nil)
}, },
1), 1),
"floor": NewFuncall( "floor": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Floor(arg[0]), nil) return NewResult(math.Floor(arg[0]), nil)
}, },
1), 1),
"gamma": NewFuncall( "gamma": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Gamma(arg[0]), nil) return NewResult(math.Gamma(arg[0]), nil)
}, },
1), 1),
"ilogb": NewFuncall( "ilogb": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(float64(math.Ilogb(arg[0])), nil) return NewResult(float64(math.Ilogb(arg[0])), nil)
}, },
1), 1),
"j0": NewFuncall( "j0": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.J0(arg[0]), nil) return NewResult(math.J0(arg[0]), nil)
}, },
1), 1),
"j1": NewFuncall( "j1": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.J1(arg[0]), nil) return NewResult(math.J1(arg[0]), nil)
}, },
1), 1),
"log": NewFuncall( "log": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Log(arg[0]), nil) return NewResult(math.Log(arg[0]), nil)
}, },
1), 1),
"log10": NewFuncall( "log10": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Log10(arg[0]), nil) return NewResult(math.Log10(arg[0]), nil)
}, },
1), 1),
"log1p": NewFuncall( "log1p": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Log1p(arg[0]), nil) return NewResult(math.Log1p(arg[0]), nil)
}, },
1), 1),
"log2": NewFuncall( "log2": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Log2(arg[0]), nil) return NewResult(math.Log2(arg[0]), nil)
}, },
1), 1),
"logb": NewFuncall( "logb": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Logb(arg[0]), nil) return NewResult(math.Logb(arg[0]), nil)
}, },
1), 1),
"pow": NewFuncall( "pow": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Pow(arg[0], arg[1]), nil) return NewResult(math.Pow(arg[0], arg[1]), nil)
}, },
2), 2),
"round": NewFuncall( "round": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Round(arg[0]), nil) return NewResult(math.Round(arg[0]), nil)
}, },
1), 1),
"roundtoeven": NewFuncall( "roundtoeven": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.RoundToEven(arg[0]), nil) return NewResult(math.RoundToEven(arg[0]), nil)
}, },
1), 1),
"sin": NewFuncall( "sin": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Sin(arg[0]), nil) return NewResult(math.Sin(arg[0]), nil)
}, },
1), 1),
"sinh": NewFuncall( "sinh": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Sinh(arg[0]), nil) return NewResult(math.Sinh(arg[0]), nil)
}, },
1), 1),
"tan": NewFuncall( "tan": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Tan(arg[0]), nil) return NewResult(math.Tan(arg[0]), nil)
}, },
1), 1),
"tanh": NewFuncall( "tanh": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Tanh(arg[0]), nil) return NewResult(math.Tanh(arg[0]), nil)
}, },
1), 1),
"trunc": NewFuncall( "trunc": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Trunc(arg[0]), nil) return NewResult(math.Trunc(arg[0]), nil)
}, },
1), 1),
"y0": NewFuncall( "y0": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Y0(arg[0]), nil) return NewResult(math.Y0(arg[0]), nil)
}, },
1), 1),
"y1": NewFuncall( "y1": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Y1(arg[0]), nil) return NewResult(math.Y1(arg[0]), nil)
}, },
1), 1),
"copysign": NewFuncall( "copysign": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Copysign(arg[0], arg[1]), nil) return NewResult(math.Copysign(arg[0], arg[1]), nil)
}, },
2), 2),
"dim": NewFuncall( "dim": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Dim(arg[0], arg[1]), nil) return NewResult(math.Dim(arg[0], arg[1]), nil)
}, },
2), 2),
"hypot": NewFuncall( "hypot": NewFuncall(
func(arg Numbers) R { func(arg Numbers) Result {
return NewR(math.Hypot(arg[0], arg[1]), nil) return NewResult(math.Hypot(arg[0], arg[1]), nil)
},
2),
// converters of all kinds
"cm-to-inch": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/2.54, nil)
},
1),
"inch-to-cm": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*2.54, nil)
},
1),
"gallons-to-liters": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*3.785, nil)
},
1),
"liters-to-gallons": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/3.785, nil)
},
1),
"yards-to-meters": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*91.44, nil)
},
1),
"meters-to-yards": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/91.44, nil)
},
1),
"miles-to-kilometers": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*1.609, nil)
},
1),
"kilometers-to-miles": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1.609, nil)
},
1),
"or": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])|int(arg[1])), nil)
},
2),
"and": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])&int(arg[1])), nil)
},
2),
"xor": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])^int(arg[1])), nil)
},
2),
"<": NewFuncall(
func(arg Numbers) Result {
// Shift by negative number provibited, so check it.
// Note that we check against uint64 overflow as well here
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewResult(0, errors.New("negative shift amount"))
}
return NewResult(float64(int(arg[0])<<int(arg[1])), nil)
},
2),
">": NewFuncall(
func(arg Numbers) Result {
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewResult(0, errors.New("negative shift amount"))
}
return NewResult(float64(int(arg[0])>>int(arg[1])), nil)
}, },
2), 2),
} }
// aliases // aliases
f["*"] = f["x"] funcmap["*"] = funcmap["x"]
f["remainder"] = f["mod"] funcmap["remainder"] = funcmap["mod"]
return f return funcmap
} }
func DefineBatchFunctions() Funcalls { func DefineBatchFunctions() Funcalls {
f := map[string]*Funcall{ funcmap := map[string]*Funcall{
"median": NewFuncall( "median": NewFuncall(
func(args Numbers) R { func(args Numbers) Result {
middle := len(args) / 2 middle := len(args) / 2
return NewR(args[middle], nil)
return NewResult(args[middle], nil)
}, },
-1), -1),
"mean": NewFuncall( "mean": NewFuncall(
func(args Numbers) R { func(args Numbers) Result {
var sum float64 var sum float64
for _, item := range args { for _, item := range args {
sum += item sum += item
} }
return NewR(sum/float64(len(args)), nil)
return NewResult(sum/float64(len(args)), nil)
}, },
-1), -1),
"min": NewFuncall( "min": NewFuncall(
func(args Numbers) R { func(args Numbers) Result {
var min float64 var min float64
min, args = args[0], args[1:] min, args = args[0], args[1:]
for _, item := range args { for _, item := range args {
@@ -424,12 +515,13 @@ func DefineBatchFunctions() Funcalls {
min = item min = item
} }
} }
return NewR(min, nil)
return NewResult(min, nil)
}, },
-1), -1),
"max": NewFuncall( "max": NewFuncall(
func(args Numbers) R { func(args Numbers) Result {
var max float64 var max float64
max, args = args[0], args[1:] max, args = args[0], args[1:]
for _, item := range args { for _, item := range args {
@@ -437,24 +529,26 @@ func DefineBatchFunctions() Funcalls {
max = item max = item
} }
} }
return NewR(max, nil)
return NewResult(max, nil)
}, },
-1), -1),
"sum": NewFuncall( "sum": NewFuncall(
func(args Numbers) R { func(args Numbers) Result {
var sum float64 var sum float64
for _, item := range args { for _, item := range args {
sum += item sum += item
} }
return NewR(sum, nil)
return NewResult(sum, nil)
}, },
-1), -1),
} }
// aliases // aliases
f["+"] = f["sum"] funcmap["+"] = funcmap["sum"]
f["avg"] = f["mean"] funcmap["avg"] = funcmap["mean"]
return f return funcmap
} }

15
go.mod
View File

@@ -1,10 +1,15 @@
module rpn module rpn
go 1.20 go 1.22
require ( require (
github.com/chzyer/readline v1.5.1 // indirect github.com/chzyer/readline v1.5.1
github.com/spf13/pflag v1.0.5 // indirect github.com/rogpeppe/go-internal v1.13.1
github.com/yuin/gopher-lua v1.1.0 // indirect github.com/spf13/pflag v1.0.5
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect github.com/yuin/gopher-lua v1.1.1
)
require (
golang.org/x/sys v0.21.0 // indirect
golang.org/x/tools v0.22.0 // indirect
) )

13
go.sum
View File

@@ -1,10 +1,17 @@
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=

View File

@@ -25,11 +25,12 @@ import (
) )
type Interpreter struct { type Interpreter struct {
debug bool debug bool
script string
} }
// LUA interpreter, instanciated in main() // LuaInterpreter is the lua interpreter, instantiated in main()
var L *lua.LState var LuaInterpreter *lua.LState
// holds a user provided lua function // holds a user provided lua function
type LuaFunction struct { type LuaFunction struct {
@@ -38,12 +39,16 @@ type LuaFunction struct {
numargs int numargs int
} }
// must be global since init() is being called from lua which doesn't // LuaFuncs must be global since init() is being called from lua which
// have access to the interpreter instance // doesn't have access to the interpreter instance
var LuaFuncs map[string]LuaFunction var LuaFuncs map[string]LuaFunction
func NewInterpreter(script string, debug bool) *Interpreter {
return &Interpreter{debug: debug, script: script}
}
// initialize the lua environment properly // initialize the lua environment properly
func InitLua(config string, debug bool) *Interpreter { func (i *Interpreter) InitLua() {
// we only load a subset of lua Open modules and don't allow // we only load a subset of lua Open modules and don't allow
// net, system or io stuff // net, system or io stuff
for _, pair := range []struct { for _, pair := range []struct {
@@ -56,8 +61,8 @@ func InitLua(config string, debug bool) *Interpreter {
{lua.DebugLibName, lua.OpenDebug}, {lua.DebugLibName, lua.OpenDebug},
{lua.MathLibName, lua.OpenMath}, {lua.MathLibName, lua.OpenMath},
} { } {
if err := L.CallByParam(lua.P{ if err := LuaInterpreter.CallByParam(lua.P{
Fn: L.NewFunction(pair.f), Fn: LuaInterpreter.NewFunction(pair.f),
NRet: 0, NRet: 0,
Protect: true, Protect: true,
}, lua.LString(pair.n)); err != nil { }, lua.LString(pair.n)); err != nil {
@@ -66,26 +71,24 @@ func InitLua(config string, debug bool) *Interpreter {
} }
// load the lua config (which we expect to contain init() and math functions) // load the lua config (which we expect to contain init() and math functions)
if err := L.DoFile(config); err != nil { if err := LuaInterpreter.DoFile(i.script); err != nil {
panic(err) panic(err)
} }
// instanciate // instantiate
LuaFuncs = map[string]LuaFunction{} LuaFuncs = map[string]LuaFunction{}
// that way the user can call register(...) from lua inside init() // that way the user can call register(...) from lua inside init()
L.SetGlobal("register", L.NewFunction(register)) LuaInterpreter.SetGlobal("register", LuaInterpreter.NewFunction(register))
// actually call init() // actually call init()
if err := L.CallByParam(lua.P{ if err := LuaInterpreter.CallByParam(lua.P{
Fn: L.GetGlobal("init"), Fn: LuaInterpreter.GetGlobal("init"),
NRet: 0, NRet: 0,
Protect: true, Protect: true,
}); err != nil { }); err != nil {
panic(err) panic(err)
} }
return &Interpreter{debug: debug}
} }
func (i *Interpreter) Debug(msg string) { func (i *Interpreter) Debug(msg string) {
@@ -105,54 +108,56 @@ func (i *Interpreter) FuncNumArgs(name string) int {
// arguments. 1 uses the last item of the stack, 2 the last two and -1 // arguments. 1 uses the last item of the stack, 2 the last two and -1
// all items (which translates to batch mode) // all items (which translates to batch mode)
// //
// The items array will be provded by calc.Eval(), these are // The items array will be provided by calc.Eval(), these are
// non-popped stack items. So the items will only removed from the // non-popped stack items. So the items will only removed from the
// stack when the lua function execution is successfull. // stack when the lua function execution is successful.
func (i *Interpreter) CallLuaFunc(funcname string, items []float64) (float64, error) { func (i *Interpreter) CallLuaFunc(funcname string, items []float64) (float64, error) {
i.Debug(fmt.Sprintf("calling lua func %s() with %d args", i.Debug(fmt.Sprintf("calling lua func %s() with %d args",
funcname, LuaFuncs[funcname].numargs)) funcname, LuaFuncs[funcname].numargs))
switch LuaFuncs[funcname].numargs { switch LuaFuncs[funcname].numargs {
case 0:
fallthrough
case 1: case 1:
// 1 arg variant // 1 arg variant
if err := L.CallByParam(lua.P{ if err := LuaInterpreter.CallByParam(lua.P{
Fn: L.GetGlobal(funcname), Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1, NRet: 1,
Protect: true, Protect: true,
}, lua.LNumber(items[0])); err != nil { }, lua.LNumber(items[0])); err != nil {
fmt.Println(err) return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
return 0, err
} }
case 2: case 2:
// 2 arg variant // 2 arg variant
if err := L.CallByParam(lua.P{ if err := LuaInterpreter.CallByParam(lua.P{
Fn: L.GetGlobal(funcname), Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1, NRet: 1,
Protect: true, Protect: true,
}, lua.LNumber(items[0]), lua.LNumber(items[1])); err != nil { }, lua.LNumber(items[0]), lua.LNumber(items[1])); err != nil {
return 0, err return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
} }
case -1: case -1:
// batch variant, use lua table as array // batch variant, use lua table as array
tb := L.NewTable() table := LuaInterpreter.NewTable()
// put the whole stack into it // put the whole stack into it
for _, item := range items { for _, item := range items {
tb.Append(lua.LNumber(item)) table.Append(lua.LNumber(item))
} }
if err := L.CallByParam(lua.P{ if err := LuaInterpreter.CallByParam(lua.P{
Fn: L.GetGlobal(funcname), Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1, NRet: 1,
Protect: true, Protect: true,
}, tb); err != nil { }, table); err != nil {
return 0, err return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
} }
} }
// get result and cast to float64 // get result and cast to float64
if res, ok := L.Get(-1).(lua.LNumber); ok { if res, ok := LuaInterpreter.Get(-1).(lua.LNumber); ok {
L.Pop(1) LuaInterpreter.Pop(1)
return float64(res), nil return float64(res), nil
} }
@@ -162,10 +167,10 @@ func (i *Interpreter) CallLuaFunc(funcname string, items []float64) (float64, er
// called from lua to register a math function numargs may be 1, 2 or // called from lua to register a math function numargs may be 1, 2 or
// -1, it denotes the number of items from the stack requested by the // -1, it denotes the number of items from the stack requested by the
// lua function. -1 means batch mode, that is all items // lua function. -1 means batch mode, that is all items
func register(L *lua.LState) int { func register(lstate *lua.LState) int {
function := L.ToString(1) function := lstate.ToString(1)
numargs := L.ToInt(2) numargs := lstate.ToInt(2)
help := L.ToString(3) help := lstate.ToString(3)
LuaFuncs[function] = LuaFunction{ LuaFuncs[function] = LuaFunction{
name: function, name: function,

95
main.go
View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2023 Thomas von Dein Copyright © 2023-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -30,25 +30,33 @@ import (
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
const VERSION string = "2.0.1" const VERSION string = "2.1.3"
const Usage string = `This is rpn, a reverse polish notation calculator cli. const Usage string = `This is rpn, a reverse polish notation calculator cli.
Usage: rpn [-bdvh] [<operator>] Usage: rpn [-bdvh] [<operator>]
Options: Options:
-b, --batchmode enable batch mode -b, --batchmode enable batch mode
-d, --debug enable debug mode -d, --debug enable debug mode
-m, --manual show manual -s, --stack show last 5 items of the stack (off by default)
-v, --version show version -i --intermediate print intermediate results
-h, --help show help -m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn + this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
Copyright (c) 2023 T.v.Dein` Copyright (c) 2023-2024 T.v.Dein`
func main() { func main() {
os.Exit(Main())
}
func Main() int {
calc := NewCalc() calc := NewCalc()
showversion := false showversion := false
@@ -58,23 +66,29 @@ func main() {
configfile := "" configfile := ""
flag.BoolVarP(&calc.batch, "batchmode", "b", false, "batch mode") flag.BoolVarP(&calc.batch, "batchmode", "b", false, "batch mode")
flag.BoolVarP(&calc.showstack, "show-stack", "s", false, "show stack")
flag.BoolVarP(&calc.intermediate, "showin-termediate", "i", false,
"show intermediate results")
flag.BoolVarP(&enabledebug, "debug", "d", false, "debug mode") flag.BoolVarP(&enabledebug, "debug", "d", false, "debug mode")
flag.BoolVarP(&showversion, "version", "v", false, "show version") flag.BoolVarP(&showversion, "version", "v", false, "show version")
flag.BoolVarP(&showhelp, "help", "h", false, "show usage") flag.BoolVarP(&showhelp, "help", "h", false, "show usage")
flag.BoolVarP(&showmanual, "manual", "m", false, "show manual") flag.BoolVarP(&showmanual, "manual", "m", false, "show manual")
flag.StringVarP(&configfile, "config", "c", flag.StringVarP(&configfile, "config", "c",
os.Getenv("HOME")+"/.rpn.lua", "config file (lua format)") os.Getenv("HOME")+"/.rpn.lua", "config file (lua format)")
flag.IntVarP(&calc.precision, "precision", "p", Precision, "floating point precision")
flag.Parse() flag.Parse()
if showversion { if showversion {
fmt.Printf("This is rpn version %s\n", VERSION) fmt.Printf("This is rpn version %s\n", VERSION)
return
return 0
} }
if showhelp { if showhelp {
fmt.Println(Usage) fmt.Println(Usage)
return
return 0
} }
if enabledebug { if enabledebug {
@@ -83,30 +97,43 @@ func main() {
if showmanual { if showmanual {
man() man()
os.Exit(0)
return 0
} }
// the lua state object is global, instanciate it early // the lua state object is global, instantiate it early
L = lua.NewState(lua.Options{SkipOpenLibs: true}) LuaInterpreter = lua.NewState(lua.Options{SkipOpenLibs: true})
defer L.Close() defer LuaInterpreter.Close()
// our config file is interpreted as lua code, only functions can // our config file is interpreted as lua code, only functions can
// be defined, init() will be called by InitLua(). // be defined, init() will be called by InitLua().
if _, err := os.Stat(configfile); err == nil { if _, err := os.Stat(configfile); err == nil {
I := InitLua(configfile, enabledebug) luarunner := NewInterpreter(configfile, enabledebug)
calc.SetInt(I) luarunner.InitLua()
calc.SetInt(luarunner)
if calc.debug {
fmt.Println("loaded config")
}
} else if calc.debug {
fmt.Println(err)
} }
if len(flag.Args()) > 1 { if len(flag.Args()) > 1 {
// commandline calc operation, no readline etc needed // commandline calc operation, no readline etc needed
// called like rpn 2 2 + // called like rpn 2 2 +
calc.stdin = true calc.stdin = true
calc.Eval(strings.Join(flag.Args(), " ")) if err := calc.Eval(strings.Join(flag.Args(), " ")); err != nil {
return fmt.Println(err)
return 1
}
return 0
} }
// interactive mode, need readline // interactive mode, need readline
rl, err := readline.NewEx(&readline.Config{ reader, err := readline.NewEx(&readline.Config{
Prompt: calc.Prompt(), Prompt: calc.Prompt(),
HistoryFile: os.Getenv("HOME") + "/.rpn-history", HistoryFile: os.Getenv("HOME") + "/.rpn-history",
HistoryLimit: 500, HistoryLimit: 500,
@@ -119,8 +146,8 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer rl.Close() defer reader.Close()
rl.CaptureExitSignal() reader.CaptureExitSignal()
if inputIsStdin() { if inputIsStdin() {
// commands are coming on stdin, however we will still enter // commands are coming on stdin, however we will still enter
@@ -130,13 +157,17 @@ func main() {
for { for {
// primary program repl // primary program repl
line, err := rl.Readline() line, err := reader.Readline()
if err != nil { if err != nil {
break break
} }
calc.Eval(line) err = calc.Eval(line)
rl.SetPrompt(calc.Prompt()) if err != nil {
fmt.Println(err)
}
reader.SetPrompt(calc.Prompt())
} }
if len(flag.Args()) > 0 { if len(flag.Args()) > 0 {
@@ -144,23 +175,31 @@ func main() {
// echo 1 2 3 4 | rpn + // echo 1 2 3 4 | rpn +
// batch mode enabled automatically // batch mode enabled automatically
calc.batch = true calc.batch = true
calc.Eval(flag.Args()[0]) if err = calc.Eval(flag.Args()[0]); err != nil {
fmt.Println(err)
return 1
}
} }
return 0
} }
func inputIsStdin() bool { func inputIsStdin() bool {
stat, _ := os.Stdin.Stat() stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0 return (stat.Mode() & os.ModeCharDevice) == 0
} }
func man() { func man() {
var buf bytes.Buffer
man := exec.Command("less", "-") man := exec.Command("less", "-")
var b bytes.Buffer buf.WriteString(manpage)
b.Write([]byte(manpage))
man.Stdout = os.Stdout man.Stdout = os.Stdout
man.Stdin = &b man.Stdin = &buf
man.Stderr = os.Stderr man.Stderr = os.Stderr
err := man.Run() err := man.Run()

20
main_test.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"os"
"testing"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"testrpn": Main,
}))
}
func TestRpn(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "t",
})
}

172
rpn.go
View File

@@ -2,16 +2,21 @@ package main
var manpage = ` var manpage = `
NAME NAME
rpn - Reverse Polish Notation Calculator for the commandline rpn - Programmable command-line calculator using reverse polish notation
SYNOPSIS SYNOPSIS
Usage: rpn [-bdvh] [<operator>] Usage: rpn [-bdvh] [<operator>]
Options: Options:
-b, --batchmode enable batch mode -b, --batchmode enable batch mode
-d, --debug enable debug mode -d, --debug enable debug mode
-v, --version show version -s, --stack show last 5 items of the stack (off by default)
-h, --help show help -i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn + this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
@@ -102,6 +107,10 @@ DESCRIPTION
If the first parameter to rpn is a math operator or function, batch mode If the first parameter to rpn is a math operator or function, batch mode
is enabled automatically, see last example. is enabled automatically, see last example.
You can enter integers, floating point numbers (positive or negative) or
hex numbers (prefixed with 0x). Time values in hh::mm format are
possible as well.
STACK MANIPULATION STACK MANIPULATION
There are lots of stack manipulation commands provided. The most There are lots of stack manipulation commands provided. The most
important one is undo which goes back to the stack before the last math important one is undo which goes back to the stack before the last math
@@ -111,7 +120,9 @@ DESCRIPTION
switch or debug toggle command), then the backup stack is also being switch or debug toggle command), then the backup stack is also being
displayed. displayed.
The stack can be reversed using the reverse command. The stack can be reversed using the reverse command. However, sometimes
only the last two values are in the wrong order. Use the swap command to
exchange them.
You can use the shift command to remove the last number from the stack. You can use the shift command to remove the last number from the stack.
@@ -119,15 +130,23 @@ DESCRIPTION
Basic operators: Basic operators:
+ add + add
- substract - subtract
/ divide / divide
x multiply (alias: *) x multiply (alias: *)
^ power ^ power
Bitwise operators:
and bitwise and
or bitwise or
xor bitwise xor
< left shift
> right shift
Percent functions: Percent functions:
% percent % percent
%- substract percent %- subtract percent
%+ add percent %+ add percent
Batch functions: Batch functions:
@@ -145,8 +164,111 @@ DESCRIPTION
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0 log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot y1 copysign dim hypot
Conversion functions:
cm-to-inch
inch-to-cm
gallons-to-liters
liters-to-gallons
yards-to-meters
meters-to-yards
miles-to-kilometers
kilometers-to-miles
Configuration Commands:
[no]batch toggle batch mode (nobatch turns it off)
[no]debug toggle debug output (nodebug turns it off)
[no]showstack show the last 5 items of the stack (noshowtack turns it off)
Show commands:
dump display the stack contents
hex show last stack item in hex form (converted to int)
history display calculation history
vars show list of variables
Stack manipulation commands:
clear clear the whole stack
shift remove the last element of the stack
reverse reverse the stack elements
swap exchange the last two stack elements
dup duplicate last stack item
undo undo last operation
edit edit the stack interactively using vi or $EDITOR
Other commands:
help|? show this message
manual show manual
quit|exit|c-d|c-c exit program
Register variables:
>NAME Put last stack element into variable NAME
<NAME Retrieve variable NAME and put onto stack
Refer to https://pkg.go.dev/math for details about those functions. Refer to https://pkg.go.dev/math for details about those functions.
There are also a number of shortcuts for some commands available:
d debug
b batch
s showstack
h history
p dump (aka print)
v vars
c clear
u undo
INTERACTIVE REPL
While you can use rpn in the command-line, the best experience you'll
have is the interactive repl (read eval print loop). Just execute "rpn"
and you'll be there.
In interactive mode you can use TAB completion to complete commands,
operators and functions. There's also a history, which allows you to
repeat complicated calculations (as long as you've entered them in one
line).
There are also a lot of key bindings, here are the most important ones:
ctrl-c + ctrl-d
Exit interactive rpn
ctrl-z
Send rpn to the backgound.
ctrl-a
Beginning of line.
ctrl-e
End of line.
ctrl-l
Clear the screen.
ctrl-r
Search through history.
COMMENTS
Lines starting with "#" are being ignored as comments. You can also
append comments to rpn input, e.g.:
# a comment
123 # another comment
In this case only 123 will be added to the stack.
VARIABLES
You can register the last item of the stack into a variable. Variable
names must be all caps. Use the ">NAME" command to put a value into
variable "NAME". Use "<NAME" to retrieve the value of variable "NAME"
and put it onto the stack.
The command vars can be used to get a list of all variables.
EXTENDING RPN USING LUA EXTENDING RPN USING LUA
You can use a lua script with lua functions to extend the calculator. By You can use a lua script with lua functions to extend the calculator. By
default the tool looks for "~/.rpn.lua". You can also specify a script default the tool looks for "~/.rpn.lua". You can also specify a script
@@ -173,8 +295,13 @@ EXTENDING RPN USING LUA
* function name * function name
* number of arguments expected (1,2 or -1 allowed), -1 means batch * number of arguments expected (see below)
mode.
Number of expected arguments can be:
- 0: expect 1 argument but do NOT modify the stack
- 1-n: do a singular calculation
- -1: batch mode work with all numbers on the stack
* help text * help text
@@ -185,6 +312,15 @@ EXTENDING RPN USING LUA
So you can't open files, execute other programs or open a connection to So you can't open files, execute other programs or open a connection to
the outside! the outside!
CONFIGURATION
rpn can be configured via command line flags (see usage above). Most of
the flags are also available as interactive commands, such as "--batch"
has the same effect as the batch command.
The floating point number precision option "-p, --precision" however is
not available as interactive command, it MUST be configured on the
command line, if needed. The default precision is 2.
GETTING HELP GETTING HELP
In interactive mode you can enter the help command (or ?) to get a short In interactive mode you can enter the help command (or ?) to get a short
help along with a list of all supported operators and functions. help along with a list of all supported operators and functions.
@@ -204,7 +340,7 @@ LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3. 3.
Copyright (c) 2023 by Thomas von Dein Copyright (c) 2023-2024 by Thomas von Dein
This software uses the following GO modules: This software uses the following GO modules:
@@ -222,17 +358,3 @@ AUTHORS
Thomas von Dein tom AT vondein DOT org Thomas von Dein tom AT vondein DOT org
` `
var usage = `
Usage: rpn [-bdvh] [<operator>]
Options:
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
`

174
rpn.pod
View File

@@ -1,16 +1,21 @@
=head1 NAME =head1 NAME
rpn - Reverse Polish Notation Calculator for the commandline rpn - Programmable command-line calculator using reverse polish notation
=head1 SYNOPSIS =head1 SYNOPSIS
Usage: rpn [-bdvh] [<operator>] Usage: rpn [-bdvh] [<operator>]
Options: Options:
-b, --batchmode enable batch mode -b, --batchmode enable batch mode
-d, --debug enable debug mode -d, --debug enable debug mode
-v, --version show version -s, --stack show last 5 items of the stack (off by default)
-h, --help show help -i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn + this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
@@ -106,6 +111,10 @@ Example of batch mode usage:
If the first parameter to rpn is a math operator or function, batch If the first parameter to rpn is a math operator or function, batch
mode is enabled automatically, see last example. mode is enabled automatically, see last example.
You can enter integers, floating point numbers (positive or negative)
or hex numbers (prefixed with 0x). Time values in hh::mm format are
possible as well.
=head2 STACK MANIPULATION =head2 STACK MANIPULATION
There are lots of stack manipulation commands provided. The most There are lots of stack manipulation commands provided. The most
@@ -116,7 +125,9 @@ You can use B<dump> to display the stack. If debugging
is enabled (C<-d> switch or B<debug> toggle command), then the backup is enabled (C<-d> switch or B<debug> toggle command), then the backup
stack is also being displayed. stack is also being displayed.
The stack can be reversed using the B<reverse> command. The stack can be reversed using the B<reverse> command. However,
sometimes only the last two values are in the wrong order. Use the
B<swap> command to exchange them.
You can use the B<shift> command to remove the last number from the You can use the B<shift> command to remove the last number from the
stack. stack.
@@ -126,15 +137,23 @@ stack.
Basic operators: Basic operators:
+ add + add
- substract - subtract
/ divide / divide
x multiply (alias: *) x multiply (alias: *)
^ power ^ power
Bitwise operators:
and bitwise and
or bitwise or
xor bitwise xor
< left shift
> right shift
Percent functions: Percent functions:
% percent % percent
%- substract percent %- subtract percent
%+ add percent %+ add percent
Batch functions: Batch functions:
@@ -152,8 +171,126 @@ Math functions:
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0 log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot y1 copysign dim hypot
Conversion functions:
cm-to-inch
inch-to-cm
gallons-to-liters
liters-to-gallons
yards-to-meters
meters-to-yards
miles-to-kilometers
kilometers-to-miles
Configuration Commands:
[no]batch toggle batch mode (nobatch turns it off)
[no]debug toggle debug output (nodebug turns it off)
[no]showstack show the last 5 items of the stack (noshowtack turns it off)
Show commands:
dump display the stack contents
hex show last stack item in hex form (converted to int)
history display calculation history
vars show list of variables
Stack manipulation commands:
clear clear the whole stack
shift remove the last element of the stack
reverse reverse the stack elements
swap exchange the last two stack elements
dup duplicate last stack item
undo undo last operation
edit edit the stack interactively using vi or $EDITOR
Other commands:
help|? show this message
manual show manual
quit|exit|c-d|c-c exit program
Register variables:
>NAME Put last stack element into variable NAME
<NAME Retrieve variable NAME and put onto stack
Refer to https://pkg.go.dev/math for details about those functions. Refer to https://pkg.go.dev/math for details about those functions.
There are also a number of shortcuts for some commands available:
d debug
b batch
s showstack
h history
p dump (aka print)
v vars
c clear
u undo
=head1 INTERACTIVE REPL
While you can use rpn in the command-line, the best experience you'll
have is the interactive repl (read eval print loop). Just execute
C<rpn> and you'll be there.
In interactive mode you can use TAB completion to complete commands,
operators and functions. There's also a history, which allows you to
repeat complicated calculations (as long as you've entered them in one
line).
There are also a lot of key bindings, here are the most important
ones:
=over
=item ctrl-c + ctrl-d
Exit interactive rpn
=item ctrl-z
Send rpn to the backgound.
=item ctrl-a
Beginning of line.
=item ctrl-e
End of line.
=item ctrl-l
Clear the screen.
=item ctrl-r
Search through history.
=back
=head1 COMMENTS
Lines starting with C<#> are being ignored as comments. You can also
append comments to rpn input, e.g.:
# a comment
123 # another comment
In this case only 123 will be added to the stack.
=head1 VARIABLES
You can register the last item of the stack into a variable. Variable
names must be all caps. Use the ">NAME" command to put a value into
variable "NAME". Use "<NAME" to retrieve the value of variable "NAME"
and put it onto the stack.
The command B<vars> can be used to get a list of all variables.
=head1 EXTENDING RPN USING LUA =head1 EXTENDING RPN USING LUA
You can use a lua script with lua functions to extend the You can use a lua script with lua functions to extend the
@@ -187,8 +324,13 @@ function name
=item * =item *
number of arguments expected (1,2 or -1 allowed), -1 means batch number of arguments expected (see below)
mode.
Number of expected arguments can be:
- 0: expect 1 argument but do NOT modify the stack
- 1-n: do a singular calculation
- -1: batch mode work with all numbers on the stack
=item * =item *
@@ -203,6 +345,16 @@ B<Please note, that io, networking and system stuff is not allowed
though. So you can't open files, execute other programs or open a though. So you can't open files, execute other programs or open a
connection to the outside!> connection to the outside!>
=head1 CONFIGURATION
B<rpn> can be configured via command line flags (see usage
above). Most of the flags are also available as interactive commands,
such as C<--batch> has the same effect as the B<batch> command.
The floating point number precision option C<-p, --precision> however
is not available as interactive command, it MUST be configured on the
command line, if needed. The default precision is 2.
=head1 GETTING HELP =head1 GETTING HELP
In interactive mode you can enter the B<help> command (or B<?>) to get In interactive mode you can enter the B<help> command (or B<?>) to get
@@ -225,7 +377,7 @@ L<https://github.com/TLINDEN/rpnc/issues>.
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3. This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2023 by Thomas von Dein Copyright (c) 2023-2024 by Thomas von Dein
This software uses the following GO modules: This software uses the following GO modules:

BIN
rpnc.mp4 Normal file

Binary file not shown.

View File

@@ -64,14 +64,14 @@ func (s *Stack) Bump() {
} }
// append an item to the stack // append an item to the stack
func (s *Stack) Push(x float64) { func (s *Stack) Push(item float64) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
s.Debug(fmt.Sprintf(" push to stack: %.2f", x)) s.Debug(fmt.Sprintf(" push to stack: %.2f", item))
s.Bump() s.Bump()
s.linklist.PushBack(x) s.linklist.PushBack(item)
} }
// remove and return an item from the stack // remove and return an item from the stack
@@ -90,6 +90,7 @@ func (s *Stack) Pop() float64 {
s.Debug(fmt.Sprintf(" remove from stack: %.2f", val)) s.Debug(fmt.Sprintf(" remove from stack: %.2f", val))
s.Bump() s.Bump()
return val.(float64) return val.(float64)
} }
@@ -115,20 +116,41 @@ func (s *Stack) Shift(num ...int) {
} }
} }
func (s *Stack) Swap() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.linklist.Len() < 2 {
return
}
prevA := s.linklist.Back()
s.linklist.Remove(prevA)
prevB := s.linklist.Back()
s.linklist.Remove(prevB)
s.Debug(fmt.Sprintf("swapping %.2f with %.2f", prevB.Value, prevA.Value))
s.linklist.PushBack(prevA.Value)
s.linklist.PushBack(prevB.Value)
}
// Return the last num items from the stack w/o modifying it. // Return the last num items from the stack w/o modifying it.
func (s *Stack) Last(num ...int) []float64 { func (s *Stack) Last(num ...int) []float64 {
items := []float64{} items := []float64{}
i := s.Len() stacklen := s.Len()
count := 1 count := 1
if len(num) > 0 { if len(num) > 0 {
count = num[0] count = num[0]
} }
for e := s.linklist.Front(); e != nil; e = e.Next() { for e := s.linklist.Front(); e != nil; e = e.Next() {
if i <= count { if stacklen <= count {
items = append(items, e.Value.(float64)) items = append(items, e.Value.(float64))
} }
i-- stacklen--
} }
return items return items
@@ -148,12 +170,14 @@ func (s *Stack) All() []float64 {
// dump the stack to stdout, including backup if debug is enabled // dump the stack to stdout, including backup if debug is enabled
func (s *Stack) Dump() { func (s *Stack) Dump() {
fmt.Printf("Stack revision %d (%p):\n", s.rev, &s.linklist) fmt.Printf("Stack revision %d (%p):\n", s.rev, &s.linklist)
for e := s.linklist.Front(); e != nil; e = e.Next() { for e := s.linklist.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value) fmt.Println(e.Value)
} }
if s.debug { if s.debug {
fmt.Printf("Backup stack revision %d (%p):\n", s.backuprev, &s.backup) fmt.Printf("Backup stack revision %d (%p):\n", s.backuprev, &s.backup)
for e := s.backup.Front(); e != nil; e = e.Next() { for e := s.backup.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value) fmt.Println(e.Value)
} }
@@ -175,31 +199,53 @@ func (s *Stack) Backup() {
// make a backup, because the elements in list.List{} are pointers // make a backup, because the elements in list.List{} are pointers
// and lead to unexpected results. The methid here works reliably // and lead to unexpected results. The methid here works reliably
// at least. // at least.
s.mutex.Lock()
defer s.mutex.Unlock()
s.Debug(fmt.Sprintf("backing up %d items from rev %d",
s.linklist.Len(), s.rev))
s.backup = list.List{} s.backup = list.List{}
for e := s.linklist.Front(); e != nil; e = e.Next() { for e := s.linklist.Front(); e != nil; e = e.Next() {
s.backup.PushBack(e.Value) s.backup.PushBack(e.Value.(float64))
} }
s.backuprev = s.rev s.backuprev = s.rev
} }
func (s *Stack) Restore() { func (s *Stack) Restore() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.rev == 0 { if s.rev == 0 {
fmt.Println("error: stack is empty.") fmt.Println("error: stack is empty.")
return return
} }
s.Debug(fmt.Sprintf("restoring stack to revision %d", s.backuprev)) s.Debug(fmt.Sprintf("restoring stack to revision %d", s.backuprev))
s.rev = s.backuprev s.rev = s.backuprev
s.linklist = s.backup
s.linklist = list.List{}
for e := s.backup.Front(); e != nil; e = e.Next() {
s.linklist.PushBack(e.Value.(float64))
}
} }
func (s *Stack) Reverse() { func (s *Stack) Reverse() {
newstack := list.List{} s.mutex.Lock()
defer s.mutex.Unlock()
items := []float64{}
for e := s.linklist.Front(); e != nil; e = e.Next() { for e := s.linklist.Front(); e != nil; e = e.Next() {
newstack.PushFront(e.Value) tail := s.linklist.Back()
items = append(items, tail.Value.(float64))
s.linklist.Remove(tail)
} }
s.linklist = newstack for i := len(items) - 1; i >= 0; i-- {
s.linklist.PushFront(items[i])
}
} }

190
stack_test.go Normal file
View File

@@ -0,0 +1,190 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"testing"
)
func TestPush(t *testing.T) {
t.Run("push", func(t *testing.T) {
s := NewStack()
s.Push(5)
if s.linklist.Back().Value != 5.0 {
t.Errorf("push failed:\n+++ got: %f\n--- want: %f",
s.linklist.Back().Value, 5.0)
}
})
}
func TestPop(t *testing.T) {
t.Run("pop", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
got := stack.Pop()
if got != 5.0 {
t.Errorf("pop failed:\n+++ got: %f\n--- want: %f",
got, 5.0)
}
if stack.Len() != 0 {
t.Errorf("stack not empty after pop()")
}
})
}
func TestPops(t *testing.T) {
t.Run("pops", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
stack.Push(5)
stack.Push(5)
stack.Pop()
if stack.Len() != 2 {
t.Errorf("stack len not correct after pop:\n+++ got: %d\n--- want: %d",
stack.Len(), 2)
}
})
}
func TestShift(t *testing.T) {
t.Run("shift", func(t *testing.T) {
stack := NewStack()
stack.Shift()
if stack.Len() != 0 {
t.Errorf("stack not empty after shift()")
}
})
}
func TestClear(t *testing.T) {
t.Run("clear", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
stack.Push(5)
stack.Push(5)
stack.Clear()
if stack.Len() != 0 {
t.Errorf("stack not empty after clear()")
}
})
}
func TestLast(t *testing.T) {
t.Run("last", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
got := stack.Last()
if len(got) != 1 {
t.Errorf("last failed:\n+++ got: %d elements\n--- want: %d elements",
len(got), 1)
}
if got[0] != 5.0 {
t.Errorf("last failed:\n+++ got: %f\n--- want: %f",
got, 5.0)
}
if stack.Len() != 1 {
t.Errorf("stack modified after last()")
}
})
}
func TestAll(t *testing.T) {
t.Run("all", func(t *testing.T) {
stack := NewStack()
list := []float64{2, 4, 6, 8}
for _, item := range list {
stack.Push(item)
}
got := stack.All()
if len(got) != len(list) {
t.Errorf("all failed:\n+++ got: %d elements\n--- want: %d elements",
len(got), len(list))
}
for i := 1; i < len(list); i++ {
if got[i] != list[i] {
t.Errorf("all failed (element %d):\n+++ got: %f\n--- want: %f",
i, got[i], list[i])
}
}
if stack.Len() != len(list) {
t.Errorf("stack modified after last()")
}
})
}
func TestBackupRestore(t *testing.T) {
t.Run("shift", func(t *testing.T) {
stack := NewStack()
stack.Push(5)
stack.Backup()
stack.Clear()
stack.Restore()
if stack.Len() != 1 {
t.Errorf("stack not correctly restored()")
}
value := stack.Pop()
if value != 5.0 {
t.Errorf("stack not identical to old revision:\n+++ got: %f\n--- want: %f",
value, 5.0)
}
})
}
func TestReverse(t *testing.T) {
t.Run("reverse", func(t *testing.T) {
stack := NewStack()
list := []float64{2, 4, 6}
reverse := []float64{6, 4, 2}
for _, item := range list {
stack.Push(item)
}
stack.Reverse()
got := stack.All()
if len(got) != len(list) {
t.Errorf("all failed:\n+++ got: %d elements\n--- want: %d elements",
len(got), len(list))
}
for i := 1; i < len(reverse); i++ {
if got[i] != reverse[i] {
t.Errorf("reverse failed (element %d):\n+++ got: %f\n--- want: %f",
i, got[i], list[i])
}
}
})
}

2
t/cmdline-command.txtar Normal file
View File

@@ -0,0 +1,2 @@
exec testrpn 1 2 dump
stdout 'Stack revision 2 .0x'

View File

@@ -0,0 +1,2 @@
! exec testrpn 1 2 dumb
stdout 'unknown command or operator'

View File

@@ -0,0 +1,2 @@
exec testrpn -p 4 2 3 /
stdout '0.6667\n'

View File

@@ -0,0 +1,2 @@
! exec testrpn 4 +
stdout 'stack doesn''t provide enough arguments'

View File

@@ -0,0 +1,2 @@
exec testrpn -d 44 55 *
stdout 'push to stack: 2420.00\n'

View File

@@ -0,0 +1,2 @@
! exec testrpn 100 50 50 - /
stdout 'division by null'

16
t/cmdlinecalc-lua.txtar Normal file
View File

@@ -0,0 +1,16 @@
exec testrpn -d -c test.lua 3 5 lower
stdout '3\n'
-- test.lua --
function lower(a,b)
if a < b then
return a
else
return b
end
end
function init()
-- expects 2 args
register("lower", 2, "lower")
end

2
t/cmdlinecalc-time.txtar Normal file
View File

@@ -0,0 +1,2 @@
exec testrpn 09:55 4:15 -
stdout '5.67\n'

2
t/cmdlinecalc.txtar Normal file
View File

@@ -0,0 +1,2 @@
exec testrpn 44 55 *
stdout '2420\n'

2
t/getman.txtar Normal file
View File

@@ -0,0 +1,2 @@
exec testrpn -m
stdout 'This software is licensed under the GNU GENERAL PUBLIC LICENSE'

2
t/getusage.txtar Normal file
View File

@@ -0,0 +1,2 @@
exec testrpn -h
stdout 'This is rpn'

2
t/getversion.txtar Normal file
View File

@@ -0,0 +1,2 @@
exec testrpn -v
stdout 'This is rpn version'

4
t/stdin-batch-cmd.txtar Normal file
View File

@@ -0,0 +1,4 @@
exec echo 1 2 3 4 5 batch median
stdin stdout
exec testrpn
[unix] stdout '3\n'

4
t/stdin-batch.txtar Normal file
View File

@@ -0,0 +1,4 @@
exec echo 1 2 3 4 5
stdin stdout
[unix] exec testrpn median
[unix] stdout '3\n'

4
t/stdin-calc.txtar Normal file
View File

@@ -0,0 +1,4 @@
exec echo 10 10 +
stdin stdout
exec testrpn
[unix] stdout '20\n'

6
t/stdin-help.txtar Normal file
View File

@@ -0,0 +1,6 @@
stdin input.txt
exec testrpn
[unix] stdout 'Available configuration commands'
-- input.txt --
?

13
t/stdin-use-vars.txtar Normal file
View File

@@ -0,0 +1,13 @@
stdin input.txt
exec testrpn
[unix] stdout '28\n'
-- input.txt --
10
10
+
>SUM
clear
8
<SUM
+

View File

@@ -0,0 +1,4 @@
exec echo 1 2 3 4 5 median
stdin stdout
exec testrpn -b
[unix] stdout '3\n'

13
t/test.lua Normal file
View File

@@ -0,0 +1,13 @@
-- simple function, return the lower number of the two operands
function lower(a,b)
if a < b then
return a
else
return b
end
end
function init()
-- expects 2 args
register("lower", 2, "lower")
end

View File

@@ -1,17 +0,0 @@
function add(a,b)
return a + b
end
function test(a)
return a
end
function parallelresistance(a,b)
return 1.0 / (a * b)
end
function init()
register("add", 2, "addition")
register("test", 1, "test")
register("parallelresistance", 2, "parallel resistance")
end

22
util.go
View File

@@ -23,13 +23,23 @@ import (
"strings" "strings"
) )
// find an item in a list // find an item in a list, generic variant
func contains(s []string, e string) bool { func contains[E comparable](s []E, v E) bool {
for _, a := range s { for _, vs := range s {
if a == e { if v == vs {
return true return true
} }
} }
return false
}
// look if a key in a map exists, generic variant
func exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false return false
} }
@@ -63,3 +73,7 @@ func const2num(name string) float64 {
func list2str(list Numbers) string { func list2str(list Numbers) string {
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]") return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]")
} }
func Error(m string) error {
return fmt.Errorf("Error: %s", m)
}

32
util_test.go Normal file
View File

@@ -0,0 +1,32 @@
/*
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"testing"
)
func TestContains(t *testing.T) {
list := []string{"a", "b", "c"}
t.Run("contains", func(t *testing.T) {
if !contains(list, "a") {
t.Errorf("a in [a,b,c] not found")
}
})
}