3 Commits

Author SHA1 Message Date
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
14 changed files with 507 additions and 82 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,7 +55,6 @@ 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 MOD=lib"
@@ -77,3 +72,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)
@@ -65,6 +69,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

70
calc.go
View File

@@ -20,7 +20,6 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -52,6 +51,8 @@ debug toggle debug output
dump display the stack contents dump display the stack contents
clear clear the whole stack clear clear the whole stack
shift remove the last element of the stack shift remove the last element of the stack
reverse reverse the stack elements
swap exchange the last two elements
history display calculation history history display calculation history
help|? show this message help|? show this message
quit|exit|c-d|c-c exit program quit|exit|c-d|c-c exit program
@@ -80,7 +81,7 @@ median median of all values`
// 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 debug undebug clear batch shift undo help history manual exit quit swap`
Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E` Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E`
) )
@@ -192,20 +193,6 @@ func (c *Calc) Eval(line string) {
c.stack.Backup() c.stack.Backup()
c.stack.Push(num) c.stack.Push(num)
} else { } else {
/*
if contains(c.MathFunctions, item) {
// go builtin math function, if implemented
c.mathfunc(item)
continue
}
if contains(c.BatchFunctions, item) {
// math functions only supported in batch mode like max or mean
c.batchfunc(item)
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()
@@ -271,6 +258,13 @@ func (c *Calc) Eval(line string) {
case "reverse": case "reverse":
c.stack.Backup() c.stack.Backup()
c.stack.Reverse() c.stack.Reverse()
case "swap":
if c.stack.Len() < 2 {
fmt.Println("stack too small, can't swap")
} else {
c.stack.Backup()
c.stack.Swap()
}
case "undo": case "undo":
c.stack.Restore() c.stack.Restore()
case "history": case "history":
@@ -376,50 +370,6 @@ func (c *Calc) Debug(msg string) {
} }
} }
// do simple calculations
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) { func (c *Calc) luafunc(funcname string) {
// called from calc loop // called from calc loop
var x float64 var x float64

141
calc_test.go Normal file
View File

@@ -0,0 +1,141 @@
/*
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"
)
func TestCalc(t *testing.T) {
calc := NewCalc()
var tests = []struct {
name string
cmd string
exp float64
batch bool
}{
{
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,
},
{
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,
},
{
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,
},
}
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)
}
})
}
}

View File

@@ -30,7 +30,7 @@ import (
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
const VERSION string = "2.0.1" const VERSION string = "2.0.2"
const Usage string = `This is rpn, a reverse polish notation calculator cli. const Usage string = `This is rpn, a reverse polish notation calculator cli.

14
rpn.go
View File

@@ -222,17 +222,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 +
`

View File

@@ -116,7 +116,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.

View File

@@ -115,6 +115,26 @@ func (s *Stack) Shift(num ...int) {
} }
} }
func (s *Stack) Swap() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.linklist.Len() < 2 {
return
}
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 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{}

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

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