diff --git a/go/calc.go b/go/calc.go new file mode 100644 index 0000000..98c10f6 --- /dev/null +++ b/go/calc.go @@ -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() +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..19dd436 --- /dev/null +++ b/go/go.mod @@ -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 +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..242b615 --- /dev/null +++ b/go/go.sum @@ -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= diff --git a/go/main.go b/go/main.go new file mode 100644 index 0000000..a82bb39 --- /dev/null +++ b/go/main.go @@ -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] [] + +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 + + +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 +} diff --git a/go/stack.go b/go/stack.go new file mode 100644 index 0000000..562ec89 --- /dev/null +++ b/go/stack.go @@ -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 +}