diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..206efb5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[bug-report]" +labels: bug +assignees: TLINDEN + +--- + +**Describtion** + + + +**Steps To Reproduce** + + + +**Expected behavior** + + + +**Version information** + + + +**Additional informations** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ed09312 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest a feature +title: "[feature-request]" +labels: feature-request +assignees: TLINDEN + +--- + +**Describtion** + + + + +**Version information** + + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..d9ceac6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/.gitignore b/.gitignore index ed13d1c..7f33a39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ releases rpn rpn.1 +coverage.out diff --git a/Makefile b/Makefile index 24131eb..ac90935 100644 --- a/Makefile +++ b/Makefile @@ -39,10 +39,6 @@ ifdef HAVE_POD echo "var manpage = \`" >> $*.go pod2text $*.pod >> $*.go echo "\`" >> $*.go - - echo "var usage = \`" >> $*.go - awk '/SYNOPS/{f=1;next} /DESCR/{f=0} f' $*.pod | sed 's/^ //' >> $*.go - echo "\`" >> $*.go endif buildlocal: @@ -59,7 +55,6 @@ clean: test: go test -v ./... - bash t/test.sh singletest: @echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib" @@ -77,3 +72,15 @@ buildall: release: buildall 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 diff --git a/README.md b/README.md index eafd073..72149e1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![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) + ## Reverse Polish Notation Calculator for the commandline This is a small commandline calculator which takes its input in diff --git a/calc.go b/calc.go index be15743..c131a7d 100644 --- a/calc.go +++ b/calc.go @@ -20,7 +20,6 @@ package main import ( "errors" "fmt" - "math" "os" "regexp" "strconv" @@ -376,50 +375,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) { // called from calc loop var x float64 diff --git a/calc_test.go b/calc_test.go new file mode 100644 index 0000000..3ad864a --- /dev/null +++ b/calc_test.go @@ -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 . +*/ + +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) + } + }) + } +} diff --git a/rpn.go b/rpn.go index 1441cdb..8890af5 100644 --- a/rpn.go +++ b/rpn.go @@ -222,17 +222,3 @@ AUTHORS Thomas von Dein tom AT vondein DOT org ` -var usage = ` - -Usage: rpn [-bdvh] [] - -Options: - -b, --batchmode enable batch mode - -d, --debug enable debug mode - -v, --version show version - -h, --help show help - -When is given, batch mode ist automatically enabled. Use -this only when working with stdin. E.g.: echo "2 3 4 5" | rpn + - -` diff --git a/stack_test.go b/stack_test.go new file mode 100644 index 0000000..aa83e96 --- /dev/null +++ b/stack_test.go @@ -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 . +*/ + +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]) + } + } + }) +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..7c37735 --- /dev/null +++ b/util_test.go @@ -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 . +*/ + +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") + } + }) +}