started work on porting it to golang

This commit is contained in:
2023-10-30 14:22:43 +01:00
parent 76a1ada486
commit 3b48674f2b
5 changed files with 397 additions and 0 deletions

155
go/calc.go Normal file
View File

@@ -0,0 +1,155 @@
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/chzyer/readline"
)
type Calc struct {
debug bool
batch bool
stdin bool
stack *Stack
completer readline.AutoCompleter
}
const Help string = `Available commands:
batch enable batch mode
debug enable debug output
dump display the stack contents
clear clear the whole stack
shift remove the last element of the stack
help show this message
Available operators:
basic operators: + - * /
`
func NewCalc() *Calc {
c := Calc{stack: NewStack(), debug: false}
c.completer = readline.NewPrefixCompleter(
readline.PcItem("dump"),
readline.PcItem("debug"),
readline.PcItem("clear"),
readline.PcItem("batch"),
readline.PcItem("shift"),
readline.PcItem("undo"),
readline.PcItem("help"),
readline.PcItem("+"),
readline.PcItem("-"),
readline.PcItem("*"),
readline.PcItem("/"),
)
return &c
}
func (c *Calc) ToggleDebug() {
c.debug = !c.debug
c.stack.ToggleDebug()
}
func (c *Calc) ToggleBatch() {
c.batch = !c.batch
}
func (c *Calc) ToggleStdin() {
c.stdin = !c.stdin
}
func (c *Calc) Eval(line string) {
line = strings.TrimSpace(line)
space := regexp.MustCompile(`\s+`)
simple := regexp.MustCompile(`[\+\-\*\/]`)
if line == "" {
return
}
for _, item := range space.Split(line, -1) {
num, err := strconv.ParseFloat(item, 64)
if err == nil {
c.stack.Backup()
c.stack.Push(num)
} else {
if simple.MatchString(line) {
c.simple(item[0])
continue
}
switch item {
case "help":
fmt.Println(Help)
case "dump":
c.stack.Dump()
case "debug":
c.ToggleDebug()
case "batch":
c.ToggleBatch()
case "clear":
c.stack.Backup()
c.stack.Clear()
case "shift":
c.stack.Backup()
c.stack.Shift()
case "undo":
c.stack.Restore()
default:
fmt.Println("unknown command or operator!")
}
}
}
}
func (c *Calc) Result() {
if !c.stdin {
fmt.Print("= ")
}
fmt.Println(c.stack.Last())
}
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
if c.debug {
fmt.Printf("DEBUG: evaluating: %.2f %c %.2f\n", a, op, b)
}
switch op {
case '+':
x = a + b
case '-':
x = a - b
case '*':
x = a * b
case '/':
if b == 0 {
fmt.Println("error: division by null!")
return
}
x = a / b
default:
panic("invalid operator!")
}
c.stack.Push(x)
if !c.batch {
break
}
}
c.Result()
}

9
go/go.mod Normal file
View File

@@ -0,0 +1,9 @@
module rpn
go 1.20
require (
github.com/chzyer/readline v1.5.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
)

8
go/go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

93
go/main.go Normal file
View File

@@ -0,0 +1,93 @@
package main
import (
"fmt"
"os"
"github.com/chzyer/readline"
flag "github.com/spf13/pflag"
)
const VERSION string = "0.0.1"
const Usage string = `This is rpn, a reverse polish notation calculator cli.
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 +
Copyright (c) 2023 T.v.Dein
`
func main() {
calc := NewCalc()
showversion := false
showhelp := false
enabledebug := false
flag.BoolVarP(&calc.batch, "batchmode", "b", false, "batch mode")
flag.BoolVarP(&enabledebug, "debug", "d", false, "debug mode")
flag.BoolVarP(&showversion, "version", "v", false, "show version")
flag.BoolVarP(&showhelp, "help", "h", false, "show usage")
flag.Parse()
if showversion {
fmt.Printf("This is rpn version %s\n", VERSION)
return
}
if showhelp {
fmt.Println(Usage)
return
}
if enabledebug {
calc.ToggleDebug()
}
rl, err := readline.NewEx(&readline.Config{
Prompt: "\033[31m»\033[0m ",
HistoryFile: os.Getenv("HOME") + ".rpn-history",
HistoryLimit: 500,
AutoComplete: calc.completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
})
if err != nil {
panic(err)
}
defer rl.Close()
rl.CaptureExitSignal()
if inputIsStdin() {
calc.ToggleStdin()
}
for {
line, err := rl.Readline()
if err != nil {
break
}
calc.Eval(line)
}
if len(flag.Args()) > 0 {
calc.Eval(flag.Args()[0])
}
}
func inputIsStdin() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0
}

132
go/stack.go Normal file
View File

@@ -0,0 +1,132 @@
package main
import (
"container/list"
"fmt"
"sync"
)
type Stack struct {
dll list.List
backup list.List
debug bool
rev int
backuprev int
mutex sync.Mutex
}
func NewStack() *Stack {
return &Stack{dll: list.List{}, backup: list.List{}, rev: 0, backuprev: 0}
}
func (s *Stack) Debug(msg string) {
if s.debug {
fmt.Printf("DEBUG(%03d): %s\n", s.rev, msg)
}
}
func (s *Stack) ToggleDebug() {
s.debug = !s.debug
}
func (s *Stack) Bump() {
s.rev++
}
func (s *Stack) Push(x float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.Debug(fmt.Sprintf(" push to stack: %.2f", x))
s.Bump()
s.dll.PushBack(x)
}
func (s *Stack) Pop() float64 {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.dll.Len() == 0 {
return 0
}
tail := s.dll.Back()
val := tail.Value
s.dll.Remove(tail)
s.Debug(fmt.Sprintf("DEBUG: remove from stack: %.2f", val))
s.Bump()
return val.(float64)
}
func (s *Stack) Shift() {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.dll.Len() == 0 {
return
}
tail := s.dll.Back()
s.dll.Remove(tail)
s.Debug(fmt.Sprintf("DEBUG: remove from stack: %.2f", tail.Value))
}
func (s *Stack) Last() float64 {
if s.dll.Back() == nil {
return 0
}
return s.dll.Back().Value.(float64)
}
func (s *Stack) Dump() {
fmt.Printf("Stack revision %d (%p):\n", s.rev, &s.dll)
for e := s.dll.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
if s.debug {
fmt.Printf("Backup stack revision %d (%p):\n", s.backuprev, &s.backup)
for e := s.backup.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}
}
func (s *Stack) Clear() {
s.Debug("DEBUG: clearing stack")
s.dll = list.List{}
}
func (s *Stack) Len() int {
return s.dll.Len()
}
func (s *Stack) Backup() {
// we need clean the list and restore it from scratch each time we
// make a backup, because the elements in list.List{} are pointers
// and lead to unexpected results. The methid here works reliably
// at least.
s.backup = list.List{}
for e := s.dll.Front(); e != nil; e = e.Next() {
s.backup.PushBack(e.Value)
}
s.backuprev = s.rev
}
func (s *Stack) Restore() {
if s.rev == 0 {
fmt.Println("error: stack is empty.")
return
}
s.Debug(fmt.Sprintf("restoring stack to revision %d", s.backuprev))
s.rev = s.backuprev
s.dll = s.backup
}