32 Commits

Author SHA1 Message Date
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
2b79f3f9ca updated docs 2023-11-05 17:39:15 +01:00
11753fc984 added the remainder of the go math functions 2023-11-05 17:38:57 +01:00
5adc3b30ab bump version 2023-11-05 13:01:50 +01:00
56a4000b67 fixed debug output 2023-11-05 13:00:26 +01:00
c4c60651d1 moved all math functions and operators to funcalls
So now if you want to add a new operator or math function all you have
to do is to add it to func.go. Conpletion will be generated from it.
2023-11-05 12:55:59 +01:00
5189d351c6 save more typing with handy types 2023-11-04 19:55:45 +01:00
3649408d17 made Shift() and Last() use flexible number of items to work on 2023-11-04 19:41:28 +01:00
a0c0a27a35 add Funcalls to Calc 2023-11-04 19:41:11 +01:00
052a9ae4d0 added more generic function management 2023-11-04 19:40:53 +01:00
48154ce6b1 fix man link, fix release generator 2023-11-02 19:36:42 +01:00
24 changed files with 2067 additions and 406 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
---
**Describtion**
<!-- 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
-->

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

@@ -0,0 +1,36 @@
name: build-and-test-rpn
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
version: [1.21]
os: [ubuntu-latest, windows-latest, macos-latest]
name: Build
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.version }}
id: go
- name: checkout
uses: actions/checkout@v3
- name: build
run: go build
- name: test
run: make test
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.21
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3

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:
@@ -59,11 +55,10 @@ clean:
test: test:
go test -v ./... go test -v ./...
bash t/test.sh
singletest: singletest:
@echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib" @echo "Call like this: ''make singletest TEST=TestPrepareColumns"
go test -run $(TEST) github.com/tlinden/rpn/$(MOD) go test -run $(TEST)
cover-report: cover-report:
go test ./... -cover -coverprofile=coverage.out go test ./... -cover -coverprofile=coverage.out
@@ -76,4 +71,16 @@ buildall:
./mkrel.sh $(tool) $(VERSION) ./mkrel.sh $(tool) $(VERSION)
release: buildall release: buildall
gh release create $(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.
@@ -254,9 +273,7 @@ hesitate to ask me about it, I'll add it.
The documentation is provided as a unix man-page. It will be The documentation is provided as a unix man-page. It will be
automatically installed if you install from source. However, you can automatically installed if you install from source. However, you can
read the man-page online: [read the man-page online](https://github.com/TLINDEN/rpnc/blob/master/rpn.pod)
https://github.com/TLINDEN/rpnc/blob/main/rpn.pod
Or if you cloned the repository you can read it this way (perl needs Or if you cloned the repository you can read it this way (perl needs
to be installed though): `perldoc rpn.pod`. to be installed though): `perldoc rpn.pod`.

623
calc.go
View File

@@ -20,9 +20,8 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
"os"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
@@ -30,57 +29,66 @@ import (
) )
type Calc struct { type Calc struct {
debug bool debug bool
batch bool batch bool
stdin bool stdin bool
stack *Stack showstack bool
history []string intermediate bool
completer readline.AutoCompleter notdone bool // set to true as long as there are items left in the eval loop
interpreter *Interpreter stack *Stack
Operators *regexp.Regexp history []string
Space *regexp.Regexp completer readline.AutoCompleter
Constants []string interpreter *Interpreter
MathFunctions []string Space *regexp.Regexp
BatchFunctions []string Comment *regexp.Regexp
LuaFunctions []string Register *regexp.Regexp
Constants []string
LuaFunctions []string
Funcalls 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 Operators:
debug toggle debug output basic operators: + - x * / ^ (* is an alias of x)
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
Available operators: Percent functions:
basic operators: + - x /
Available math functions:
sqrt square root
mod remainder of division (alias: remainder)
max batch mode only: max of all values
min batch mode only: min of all values
mean batch mode only: mean of all values (alias: avg)
median batch mode only: median of all values
% percent % percent
%- substract percent %- substract percent
%+ add percent %+ add percent
Math operators: Math functions (see https://pkg.go.dev/math):
^ power` mod sqrt abs acos acosh asin asinh atan atan2 atanh cbrt ceil cos cosh
erf erfc erfcinv erfinv exp exp2 expm1 floor gamma ilogb j0 j1 log
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot
Batch functions:
sum sum of all values (alias: +)
max max of all values
min min of all values
mean mean of all values (alias: avg)
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` //Commands string = `dump reverse clear shift undo help history manual exit quit swap debug undebug nodebug batch nobatch showstack noshowstack vars`
Operators string = `+ - * x / ^ % %- %+` Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E`
MathFunctions string = `sqrt remainder`
Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E`
BatchFunctions string = `median avg mean max min`
) )
// That way we can add custom functions to completion // That way we can add custom functions to completion
@@ -92,53 +100,77 @@ func GetCompleteCustomFunctions() func(string) []string {
completions = append(completions, luafunc) completions = append(completions, luafunc)
} }
completions = append(completions, strings.Split(Commands, " ")...)
completions = append(completions, strings.Split(Operators, " ")...)
completions = append(completions, strings.Split(MathFunctions, " ")...)
completions = append(completions, strings.Split(Constants, " ")...) completions = append(completions, strings.Split(Constants, " ")...)
return completions return completions
} }
} }
func (c *Calc) GetCompleteCustomFuncalls() func(string) []string {
return func(line string) []string {
completions := []string{}
for function := range c.Funcalls {
completions = append(completions, function)
}
for function := range c.BatchFuncalls {
completions = append(completions, function)
}
for command := range c.SettingsCommands {
completions = append(completions, command)
}
for command := range c.ShowCommands {
completions = append(completions, command)
}
for command := range c.StackCommands {
completions = append(completions, command)
}
for command := range c.Commands {
completions = append(completions, command)
}
return completions
}
}
func NewCalc() *Calc { func NewCalc() *Calc {
c := Calc{stack: NewStack(), debug: false} c := Calc{stack: NewStack(), debug: false}
c.Funcalls = DefineFunctions()
c.BatchFuncalls = DefineBatchFunctions()
c.Vars = map[string]float64{}
c.completer = readline.NewPrefixCompleter( c.completer = readline.NewPrefixCompleter(
// custom lua functions // custom lua functions
readline.PcItemDynamic(GetCompleteCustomFunctions()), readline.PcItemDynamic(GetCompleteCustomFunctions()),
readline.PcItemDynamic(c.GetCompleteCustomFuncalls()),
) )
// pre-calculate mode switching regexes
reg := `^[`
for _, op := range strings.Split(Operators, " ") {
switch op {
case "x":
reg += op
default:
reg += `\` + op
}
}
reg += `]$`
c.Operators = regexp.MustCompile(reg)
c.Space = regexp.MustCompile(`\s+`) c.Space = regexp.MustCompile(`\s+`)
c.Comment = regexp.MustCompile(`#.*`) // ignore everything after #
c.Register = regexp.MustCompile(`^([<>])([A-Z][A-Z0-9]*)`)
// pre-calculate mode switching arrays // pre-calculate mode switching arrays
c.Constants = strings.Split(Constants, " ") c.Constants = strings.Split(Constants, " ")
c.MathFunctions = strings.Split(MathFunctions, " ")
c.BatchFunctions = strings.Split(BatchFunctions, " ")
for name := range LuaFuncs { c.SetCommands()
c.LuaFunctions = append(c.LuaFunctions, name)
}
return &c return &c
} }
// setup the interpreter, called from main() // setup the interpreter, called from main(), import lua functions
func (c *Calc) SetInt(I *Interpreter) { func (c *Calc) SetInt(I *Interpreter) {
c.interpreter = I c.interpreter = I
for name := range LuaFuncs {
c.LuaFunctions = append(c.LuaFunctions, name)
}
} }
func (c *Calc) ToggleDebug() { func (c *Calc) ToggleDebug() {
@@ -156,14 +188,21 @@ 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 " p := "\033[31m»\033[0m "
b := "" b := ""
if c.batch { if c.batch {
b = "->batch" b = "->batch"
} }
d := "" d := ""
v := "" v := ""
if c.debug { if c.debug {
d = "->debug" d = "->debug"
v = fmt.Sprintf("/rev%d", c.stack.rev) v = fmt.Sprintf("/rev%d", c.stack.rev)
@@ -174,25 +213,28 @@ func (c *Calc) Prompt() string {
// 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) {
line = strings.TrimSpace(line) // remove surrounding whitespace and comments, if any
line = strings.TrimSpace(c.Comment.ReplaceAllString(line, ""))
if line == "" { if line == "" {
return return
} }
for _, item := range c.Space.Split(line, -1) { items := c.Space.Split(line, -1)
for pos, item := range items {
if pos+1 < len(items) {
c.notdone = true
} else {
c.notdone = false
}
num, err := strconv.ParseFloat(item, 64) num, err := strconv.ParseFloat(item, 64)
if err == nil { if err == nil {
c.stack.Backup() c.stack.Backup()
c.stack.Push(num) c.stack.Push(num)
} else { } else {
if c.Operators.MatchString(item) {
// simple ops like + or x
c.simple(item[0])
continue
}
if contains(c.Constants, item) { if contains(c.Constants, item) {
// put the constant onto the stack // put the constant onto the stack
c.stack.Backup() c.stack.Backup()
@@ -200,68 +242,157 @@ func (c *Calc) Eval(line string) {
continue continue
} }
if contains(c.MathFunctions, item) { if _, ok := c.Funcalls[item]; ok {
// go builtin math function, if implemented if err := c.DoFuncall(item); err != nil {
c.mathfunc(item) fmt.Println(err)
} else {
c.Result()
}
continue continue
} }
if contains(c.BatchFunctions, item) { if c.batch {
// math functions only supported in batch mode like max or mean if _, ok := c.BatchFuncalls[item]; ok {
c.batchfunc(item) if err := c.DoFuncall(item); err != nil {
continue 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) { if contains(c.LuaFunctions, item) {
// user provided custom lua functions // user provided custom lua functions
c.luafunc(item) c.EvalLuaFunction(item)
continue
}
regmatches := c.Register.FindStringSubmatch(item)
if len(regmatches) == 3 {
switch regmatches[1] {
case ">":
c.PutVar(regmatches[2])
case "<":
c.GetVar(regmatches[2])
}
continue
}
// internal commands
if _, ok := c.Commands[item]; ok {
c.Commands[item].Func(c)
continue
}
if _, ok := c.ShowCommands[item]; ok {
c.ShowCommands[item].Func(c)
continue
}
if _, ok := c.StackCommands[item]; ok {
c.StackCommands[item].Func(c)
continue
}
if _, ok := c.SettingsCommands[item]; ok {
c.SettingsCommands[item].Func(c)
continue continue
} }
// management commands
switch item { switch item {
case "?": case "?":
fallthrough fallthrough
case "help": case "help":
fmt.Println(Help) c.PrintHelp()
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: default:
fmt.Println("unknown command or operator!") fmt.Println("unknown command or operator!")
} }
} }
} }
if c.showstack && !c.stdin {
dots := ""
if c.stack.Len() > 5 {
dots = "... "
}
last := c.stack.Last(5)
fmt.Printf("stack: %s%s\n", dots, list2str(last))
}
}
// Execute a math function, check if it is defined just in case
func (c *Calc) DoFuncall(funcname string) error {
var function *Funcall
if c.batch {
function = c.BatchFuncalls[funcname]
} else {
function = c.Funcalls[funcname]
}
if function == nil {
panic("function not defined but in completion list")
}
var args Numbers
batch := false
if function.Expectargs == -1 {
// batch mode, but always < stack len, so check first
args = c.stack.All()
batch = true
} else {
// this is way better behavior than just using 0 in place of
// non-existing stack items
if c.stack.Len() < function.Expectargs {
return errors.New("stack doesn't provide enough arguments")
}
args = c.stack.Last(function.Expectargs)
}
c.Debug(fmt.Sprintf("calling %s with args: %v", funcname, args))
// the actual lambda call, so to say. We provide a slice of
// the requested size, fetched from the stack (but not popped
// yet!)
R := function.Func(args)
if R.Err != nil {
// leave the stack untouched in case of any error
return R.Err
}
// don't forget to backup!
c.stack.Backup()
// "pop"
if batch {
// get rid of stack
c.stack.Clear()
} else {
// remove operands
c.stack.Shift(function.Expectargs)
}
// save result
c.stack.Push(R.Res)
// thanks a lot
c.SetHistory(funcname, args, R.Res)
return nil
}
// we need to add a history entry for each operation
func (c *Calc) SetHistory(op string, args Numbers, res float64) {
c.History("%s %s -> %f", list2str(args), op, res)
} }
// just a textual representation of math operations, viewable with the // just a textual representation of math operations, viewable with the
@@ -272,13 +403,18 @@ 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])
} }
fmt.Println(c.stack.Last()) return c.stack.Last()[0]
return c.stack.Last()
} }
func (c *Calc) Debug(msg string) { func (c *Calc) Debug(msg string) {
@@ -287,176 +423,18 @@ 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()
}
// calc using go math lib functions
func (c *Calc) mathfunc(funcname string) {
c.stack.Backup()
for c.stack.Len() > 0 {
var x float64
switch funcname {
case "sqrt":
a := c.stack.Pop()
x = math.Sqrt(a)
c.History("sqrt(%f) = %f", a, x)
case "mod":
fallthrough // alias
case "remainder":
b := c.stack.Pop()
a := c.stack.Pop()
x = math.Remainder(a, b)
c.History("remainderf(%f / %f) = %f", a, b, x)
case "%":
b := c.stack.Pop()
a := c.stack.Pop()
x = (a / 100) * b
c.History("%f percent of %f = %f", b, a, x)
case "%-":
b := c.stack.Pop()
a := c.stack.Pop()
x = a - ((a / 100) * b)
c.History("%f minus %f percent of %f = %f", a, b, a, x)
case "%+":
b := c.stack.Pop()
a := c.stack.Pop()
x = a + ((a / 100) * b)
c.History("%f plus %f percent of %f = %f", a, b, a, x)
}
c.stack.Push(x)
if !c.batch {
break
}
}
c.Result()
}
// execute pure batch functions, operating on the whole stack
func (c *Calc) batchfunc(funcname string) {
if !c.batch {
fmt.Println("error: only available in batch mode")
}
c.stack.Backup()
var x float64
count := c.stack.Len()
switch funcname {
case "median":
all := []float64{}
for c.stack.Len() > 0 {
all = append(all, c.stack.Pop())
}
middle := count / 2
x = all[middle]
c.History("median(all)")
case "mean":
fallthrough // alias
case "avg":
var sum float64
for c.stack.Len() > 0 {
sum += c.stack.Pop()
}
x = sum / float64(count)
c.History("avg(all)")
case "min":
x = c.stack.Pop() // initialize with the last one
for c.stack.Len() > 0 {
val := c.stack.Pop()
if val < x {
x = val
}
}
c.History("min(all)")
case "max":
x = c.stack.Pop() // initialize with the last one
for c.stack.Len() > 0 {
val := c.stack.Pop()
if val > x {
x = val
}
}
c.History("max(all)")
}
c.stack.Push(x)
_ = c.Result()
}
func (c *Calc) luafunc(funcname string) {
// called from calc loop // called from calc loop
var x float64 var x 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, []float64{c.stack.Last()}) x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last())
case 2: case 2:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.LastTwo()) x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last(2))
case -1: case -1:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.All()) x, err = c.interpreter.CallLuaFunc(funcname, c.stack.All())
default: default:
@@ -470,7 +448,15 @@ func (c *Calc) luafunc(funcname string) {
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, x)
}
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, x)
@@ -483,7 +469,78 @@ func (c *Calc) luafunc(funcname string) {
c.History("%s(*) = %f", funcname, x) c.History("%s(*) = %f", funcname, x)
} }
c.stack.Push(x) if dopush {
c.stack.Push(x)
}
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 _, ok := c.Vars[name]; ok {
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 {
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)
}
}
}

313
calc_test.go Normal file
View File

@@ -0,0 +1,313 @@
/*
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"
"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 _, tt := range tests {
testname := fmt.Sprintf("%s .(expect %.2f)",
tt.name, tt.exp)
t.Run(testname, func(t *testing.T) {
for _, line := range tt.cmd {
calc.Eval(line)
}
got := calc.stack.Last()
if len(got) > 0 {
if got[0] != tt.exp {
t.Errorf("parsing failed:\n+++ got: %f\n--- want: %f",
got, tt.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,
},
}
for _, tt := range tests {
testname := fmt.Sprintf("cmd-%s-expect-%.2f",
tt.name, tt.exp)
t.Run(testname, func(t *testing.T) {
calc.batch = tt.batch
calc.Eval(tt.cmd)
got := calc.Result()
calc.stack.Clear()
if got != tt.exp {
t.Errorf("calc failed:\n+++ got: %f\n--- want: %f",
got, tt.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()
L = lua.NewState(lua.Options{SkipOpenLibs: true})
defer L.Close()
luarunner := NewInterpreter("example.lua", false)
luarunner.InitLua()
calc.SetInt(luarunner)
for _, tt := range tests {
testname := fmt.Sprintf("lua-%s", tt.function)
t.Run(testname, func(t *testing.T) {
calc.stack.Clear()
for _, item := range tt.stack {
calc.stack.Push(item)
}
calc.EvalLuaFunction(tt.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] != tt.exp {
t.Errorf("lua function %s failed:\n+++ got: %f\n--- want: %f",
tt.function, got, tt.exp)
}
})
}
}

201
command.go Normal file
View File

@@ -0,0 +1,201 @@
/*
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"
"os"
)
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,
}
}
// define all management (that is: non calculation) commands
func (c *Calc) SetCommands() {
c.SettingsCommands = 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
},
),
}
c.ShowCommands = 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")
}
},
),
}
c.StackCommands = 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",
func(c *Calc) {
if c.stack.Len() < 2 {
fmt.Println("stack too small, can't swap")
} else {
c.stack.Backup()
c.stack.Swap()
}
},
),
"undo": NewCommand(
"undo last operation",
func(c *Calc) {
c.stack.Restore()
},
),
"dup": NewCommand(
"duplicate last stack item",
func(c *Calc) {
item := c.stack.Last()
if len(item) == 1 {
c.stack.Backup()
c.stack.Push(item[0])
} else {
fmt.Println("stack empty")
}
},
),
}
// 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["undebug"] = c.SettingsCommands["nodebug"]
c.SettingsCommands["show"] = c.SettingsCommands["showstack"]
}

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

460
funcs.go Normal file
View File

@@ -0,0 +1,460 @@
/*
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 (
"errors"
"math"
)
type R struct {
Res float64
Err error
}
type Numbers []float64
type Function func(Numbers) R
// 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
// function to be executed.
//
// The function has to take a float slice as argument and return a
// float and an error object. The float slice is guaranteed to have
// the expected number of arguments.
//
// However, Lua functions are handled differently, see interpreter.go.
type Funcall struct {
Expectargs int // -1 means batch only mode, you'll get the whole stack as arg
Func Function
}
// will hold all hard coded functions and operators
type Funcalls map[string]*Funcall
// convenience function, create a new Funcall object, if expectargs
// was not specified, 2 is assumed.
func NewFuncall(function Function, expectargs ...int) *Funcall {
expect := 2
if len(expectargs) > 0 {
expect = expectargs[0]
}
return &Funcall{
Expectargs: expect,
Func: function,
}
}
// Convenience function, create new result
func NewR(n float64, e error) R {
return R{Res: n, Err: e}
}
// the actual functions, called once during initialization.
func DefineFunctions() Funcalls {
f := map[string]*Funcall{
// simple operators, they all expect 2 args
"+": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]+arg[1], nil)
},
),
"-": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]-arg[1], nil)
},
),
"x": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]*arg[1], nil)
},
),
"/": NewFuncall(
func(arg Numbers) R {
if arg[1] == 0 {
return NewR(0, errors.New("division by null"))
}
return NewR(arg[0]/arg[1], nil)
},
),
"^": NewFuncall(
func(arg Numbers) R {
return NewR(math.Pow(arg[0], arg[1]), nil)
},
),
"%": NewFuncall(
func(arg Numbers) R {
return NewR((arg[0]/100)*arg[1], nil)
},
),
"%-": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]-((arg[0]/100)*arg[1]), nil)
},
),
"%+": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]+((arg[0]/100)*arg[1]), nil)
},
),
"mod": NewFuncall(
func(arg Numbers) R {
return NewR(math.Remainder(arg[0], arg[1]), nil)
},
),
"sqrt": NewFuncall(
func(arg Numbers) R {
return NewR(math.Sqrt(arg[0]), nil)
},
1),
"abs": NewFuncall(
func(arg Numbers) R {
return NewR(math.Abs(arg[0]), nil)
},
1),
"acos": NewFuncall(
func(arg Numbers) R {
return NewR(math.Acos(arg[0]), nil)
},
1),
"acosh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Acosh(arg[0]), nil)
},
1),
"asin": NewFuncall(
func(arg Numbers) R {
return NewR(math.Asin(arg[0]), nil)
},
1),
"asinh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Asinh(arg[0]), nil)
},
1),
"atan": NewFuncall(
func(arg Numbers) R {
return NewR(math.Atan(arg[0]), nil)
},
1),
"atan2": NewFuncall(
func(arg Numbers) R {
return NewR(math.Atan2(arg[0], arg[1]), nil)
},
2),
"atanh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Atanh(arg[0]), nil)
},
1),
"cbrt": NewFuncall(
func(arg Numbers) R {
return NewR(math.Cbrt(arg[0]), nil)
},
1),
"ceil": NewFuncall(
func(arg Numbers) R {
return NewR(math.Ceil(arg[0]), nil)
},
1),
"cos": NewFuncall(
func(arg Numbers) R {
return NewR(math.Cos(arg[0]), nil)
},
1),
"cosh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Cosh(arg[0]), nil)
},
1),
"erf": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erf(arg[0]), nil)
},
1),
"erfc": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erfc(arg[0]), nil)
},
1),
"erfcinv": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erfcinv(arg[0]), nil)
},
1),
"erfinv": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erfinv(arg[0]), nil)
},
1),
"exp": NewFuncall(
func(arg Numbers) R {
return NewR(math.Exp(arg[0]), nil)
},
1),
"exp2": NewFuncall(
func(arg Numbers) R {
return NewR(math.Exp2(arg[0]), nil)
},
1),
"expm1": NewFuncall(
func(arg Numbers) R {
return NewR(math.Expm1(arg[0]), nil)
},
1),
"floor": NewFuncall(
func(arg Numbers) R {
return NewR(math.Floor(arg[0]), nil)
},
1),
"gamma": NewFuncall(
func(arg Numbers) R {
return NewR(math.Gamma(arg[0]), nil)
},
1),
"ilogb": NewFuncall(
func(arg Numbers) R {
return NewR(float64(math.Ilogb(arg[0])), nil)
},
1),
"j0": NewFuncall(
func(arg Numbers) R {
return NewR(math.J0(arg[0]), nil)
},
1),
"j1": NewFuncall(
func(arg Numbers) R {
return NewR(math.J1(arg[0]), nil)
},
1),
"log": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log(arg[0]), nil)
},
1),
"log10": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log10(arg[0]), nil)
},
1),
"log1p": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log1p(arg[0]), nil)
},
1),
"log2": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log2(arg[0]), nil)
},
1),
"logb": NewFuncall(
func(arg Numbers) R {
return NewR(math.Logb(arg[0]), nil)
},
1),
"pow": NewFuncall(
func(arg Numbers) R {
return NewR(math.Pow(arg[0], arg[1]), nil)
},
2),
"round": NewFuncall(
func(arg Numbers) R {
return NewR(math.Round(arg[0]), nil)
},
1),
"roundtoeven": NewFuncall(
func(arg Numbers) R {
return NewR(math.RoundToEven(arg[0]), nil)
},
1),
"sin": NewFuncall(
func(arg Numbers) R {
return NewR(math.Sin(arg[0]), nil)
},
1),
"sinh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Sinh(arg[0]), nil)
},
1),
"tan": NewFuncall(
func(arg Numbers) R {
return NewR(math.Tan(arg[0]), nil)
},
1),
"tanh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Tanh(arg[0]), nil)
},
1),
"trunc": NewFuncall(
func(arg Numbers) R {
return NewR(math.Trunc(arg[0]), nil)
},
1),
"y0": NewFuncall(
func(arg Numbers) R {
return NewR(math.Y0(arg[0]), nil)
},
1),
"y1": NewFuncall(
func(arg Numbers) R {
return NewR(math.Y1(arg[0]), nil)
},
1),
"copysign": NewFuncall(
func(arg Numbers) R {
return NewR(math.Copysign(arg[0], arg[1]), nil)
},
2),
"dim": NewFuncall(
func(arg Numbers) R {
return NewR(math.Dim(arg[0], arg[1]), nil)
},
2),
"hypot": NewFuncall(
func(arg Numbers) R {
return NewR(math.Hypot(arg[0], arg[1]), nil)
},
2),
}
// aliases
f["*"] = f["x"]
f["remainder"] = f["mod"]
return f
}
func DefineBatchFunctions() Funcalls {
f := map[string]*Funcall{
"median": NewFuncall(
func(args Numbers) R {
middle := len(args) / 2
return NewR(args[middle], nil)
},
-1),
"mean": NewFuncall(
func(args Numbers) R {
var sum float64
for _, item := range args {
sum += item
}
return NewR(sum/float64(len(args)), nil)
},
-1),
"min": NewFuncall(
func(args Numbers) R {
var min float64
min, args = args[0], args[1:]
for _, item := range args {
if item < min {
min = item
}
}
return NewR(min, nil)
},
-1),
"max": NewFuncall(
func(args Numbers) R {
var max float64
max, args = args[0], args[1:]
for _, item := range args {
if item > max {
max = item
}
}
return NewR(max, nil)
},
-1),
"sum": NewFuncall(
func(args Numbers) R {
var sum float64
for _, item := range args {
sum += item
}
return NewR(sum, nil)
},
-1),
}
// aliases
f["+"] = f["sum"]
f["avg"] = f["mean"]
return f
}

View File

@@ -25,7 +25,8 @@ import (
) )
type Interpreter struct { type Interpreter struct {
debug bool debug bool
script string
} }
// LUA interpreter, instanciated in main() // LUA interpreter, instanciated in main()
@@ -42,8 +43,12 @@ type LuaFunction struct {
// have access to the interpreter instance // 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 {
@@ -66,7 +71,7 @@ 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 := L.DoFile(i.script); err != nil {
panic(err) panic(err)
} }
@@ -84,8 +89,6 @@ func InitLua(config string, debug bool) *Interpreter {
}); err != nil { }); err != nil {
panic(err) panic(err)
} }
return &Interpreter{debug: debug}
} }
func (i *Interpreter) Debug(msg string) { func (i *Interpreter) Debug(msg string) {
@@ -113,6 +116,8 @@ func (i *Interpreter) CallLuaFunc(funcname string, items []float64) (float64, er
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 := L.CallByParam(lua.P{

22
main.go
View File

@@ -30,18 +30,20 @@ import (
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
const VERSION string = "2.0.0" const VERSION string = "2.0.9"
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
-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 +
@@ -58,6 +60,9 @@ 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")
@@ -93,8 +98,9 @@ func main() {
// 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 len(flag.Args()) > 1 { if len(flag.Args()) > 1 {

141
rpn.go
View File

@@ -2,16 +2,19 @@ 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
-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 +
@@ -111,25 +114,110 @@ 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.
BUILTIN OPERATORS AND FUNCTIONS BUILTIN OPERATORS AND FUNCTIONS
Basic operators: + - x / Basic operators:
Math functions: + add
- substract
/ divide
x multiply (alias: *)
^ power
Percent functions:
sqrt square root
mod remainder of division (alias: remainder)
max batch mode only: max of all values
min batch mode only: min of all values
mean batch mode only: mean of all values (alias: avg)
median batch mode only: median of all values
% percent % percent
%- substract percent %- substract percent
%+ add percent %+ add percent
^ power
Batch functions:
sum sum of all values (alias: +)
max max of all values
min min of all values
mean mean of all values (alias: avg)
median median of all values
Math functions:
mod sqrt abs acos acosh asin asinh atan atan2 atanh cbrt ceil cos cosh
erf erfc erfcinv erfinv exp exp2 expm1 floor gamma ilogb j0 j1 log
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot
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)
dump display the stack contents
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
history display calculation history
help|? show this message
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.
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
@@ -157,8 +245,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
@@ -206,17 +299,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 +
`

142
rpn.pod
View File

@@ -1,16 +1,19 @@
=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
-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 +
@@ -116,27 +119,127 @@ 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.
=head2 BUILTIN OPERATORS AND FUNCTIONS =head2 BUILTIN OPERATORS AND FUNCTIONS
Basic operators: + - x / Basic operators:
Math functions: + add
- substract
/ divide
x multiply (alias: *)
^ power
Percent functions:
sqrt square root
mod remainder of division (alias: remainder)
max batch mode only: max of all values
min batch mode only: min of all values
mean batch mode only: mean of all values (alias: avg)
median batch mode only: median of all values
% percent % percent
%- substract percent %- substract percent
%+ add percent %+ add percent
^ power
Batch functions:
sum sum of all values (alias: +)
max max of all values
min min of all values
mean mean of all values (alias: avg)
median median of all values
Math functions:
mod sqrt abs acos acosh asin asinh atan atan2 atanh cbrt ceil cos cosh
erf erfc erfcinv erfinv exp exp2 expm1 floor gamma ilogb j0 j1 log
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot
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)
dump display the stack contents
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
history display calculation history
help|? show this message
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.
=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
@@ -171,8 +274,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 *

View File

@@ -94,46 +94,63 @@ func (s *Stack) Pop() float64 {
} }
// just remove the last item, do not return it // just remove the last item, do not return it
func (s *Stack) Shift() { func (s *Stack) Shift(num ...int) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
count := 1
if len(num) > 0 {
count = num[0]
}
if s.linklist.Len() == 0 { if s.linklist.Len() == 0 {
return return
} }
tail := s.linklist.Back() for i := 0; i < count; i++ {
s.linklist.Remove(tail) tail := s.linklist.Back()
s.linklist.Remove(tail)
s.Debug(fmt.Sprintf("remove from stack: %.2f", tail.Value)) s.Debug(fmt.Sprintf("remove from stack: %.2f", tail.Value))
}
} }
// just return the last item, do not remove it func (s *Stack) Swap() {
func (s *Stack) Last() float64 { s.mutex.Lock()
if s.linklist.Back() == nil { defer s.mutex.Unlock()
return 0
if s.linklist.Len() < 2 {
return
} }
return s.linklist.Back().Value.(float64) a := s.linklist.Back()
s.linklist.Remove(a)
b := s.linklist.Back()
s.linklist.Remove(b)
s.Debug(fmt.Sprintf("swapping %.2f with %.2f", b.Value, a.Value))
s.linklist.PushBack(a.Value)
s.linklist.PushBack(b.Value)
} }
// Return the last 2 elements of the stack without modifying it. // Return the last num items from the stack w/o modifying it.
// func (s *Stack) Last(num ...int) []float64 {
// We need to return the last 2 elements of the stack, however
// container/list only supports access to 1 last element. So, we
// pop the last, retrieve the second last and push the popped one
// back.
func (s *Stack) LastTwo() []float64 {
items := []float64{} items := []float64{}
if s.linklist.Back() == nil { i := s.Len()
return items count := 1
if len(num) > 0 {
count = num[0]
} }
last := s.Pop() for e := s.linklist.Front(); e != nil; e = e.Next() {
items = append(items, last) if i <= count {
items = append(items, s.linklist.Back().Value.(float64)) items = append(items, e.Value.(float64))
}
i--
}
s.Push(last)
return items return items
} }
@@ -164,7 +181,7 @@ func (s *Stack) Dump() {
} }
func (s *Stack) Clear() { func (s *Stack) Clear() {
s.Debug("DEBUG: clearing stack") s.Debug("clearing stack")
s.linklist = list.List{} s.linklist = list.List{}
} }
@@ -178,14 +195,24 @@ 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
@@ -194,15 +221,26 @@ func (s *Stack) Restore() {
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) {
s := NewStack()
s.Push(5)
got := s.Pop()
if got != 5.0 {
t.Errorf("pop failed:\n+++ got: %f\n--- want: %f",
got, 5.0)
}
if s.Len() != 0 {
t.Errorf("stack not empty after pop()")
}
})
}
func TestPops(t *testing.T) {
t.Run("pops", func(t *testing.T) {
s := NewStack()
s.Push(5)
s.Push(5)
s.Push(5)
s.Pop()
if s.Len() != 2 {
t.Errorf("stack len not correct after pop:\n+++ got: %d\n--- want: %d",
s.Len(), 2)
}
})
}
func TestShift(t *testing.T) {
t.Run("shift", func(t *testing.T) {
s := NewStack()
s.Shift()
if s.Len() != 0 {
t.Errorf("stack not empty after shift()")
}
})
}
func TestClear(t *testing.T) {
t.Run("clear", func(t *testing.T) {
s := NewStack()
s.Push(5)
s.Push(5)
s.Push(5)
s.Clear()
if s.Len() != 0 {
t.Errorf("stack not empty after clear()")
}
})
}
func TestLast(t *testing.T) {
t.Run("last", func(t *testing.T) {
s := NewStack()
s.Push(5)
got := s.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 s.Len() != 1 {
t.Errorf("stack modified after last()")
}
})
}
func TestAll(t *testing.T) {
t.Run("all", func(t *testing.T) {
s := NewStack()
list := []float64{2, 4, 6, 8}
for _, item := range list {
s.Push(item)
}
got := s.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 s.Len() != len(list) {
t.Errorf("stack modified after last()")
}
})
}
func TestBackupRestore(t *testing.T) {
t.Run("shift", func(t *testing.T) {
s := NewStack()
s.Push(5)
s.Backup()
s.Clear()
s.Restore()
if s.Len() != 1 {
t.Errorf("stack not correctly restored()")
}
a := s.Pop()
if a != 5.0 {
t.Errorf("stack not identical to old revision:\n+++ got: %f\n--- want: %f",
a, 5.0)
}
})
}
func TestReverse(t *testing.T) {
t.Run("reverse", func(t *testing.T) {
s := NewStack()
list := []float64{2, 4, 6}
reverse := []float64{6, 4, 2}
for _, item := range list {
s.Push(item)
}
s.Reverse()
got := s.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])
}
}
})
}

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

10
util.go
View File

@@ -17,7 +17,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package main
import "math" import (
"fmt"
"math"
"strings"
)
// find an item in a list // find an item in a list
func contains(s []string, e string) bool { func contains(s []string, e string) bool {
@@ -55,3 +59,7 @@ func const2num(name string) float64 {
return 0 return 0
} }
} }
func list2str(list Numbers) string {
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]")
}

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")
}
})
}