Files
rpnc/go/calc.go

381 lines
6.6 KiB
Go

package main
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
"github.com/chzyer/readline"
lua "github.com/yuin/gopher-lua"
)
type Calc struct {
debug bool
batch bool
stdin bool
stack *Stack
history []string
completer readline.AutoCompleter
L *lua.LState
}
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
history display calculation history
help show this message
Available operators:
basic operators: + - * /
Math operators:
^ power`
// That way I can add custom functions to completion
func GetCompleteCustomFunctions() func(string) []string {
return func(line string) []string {
funcs := []string{}
for luafunc := range LuaFuncs {
funcs = append(funcs, luafunc)
}
return funcs
}
}
func NewCalc() *Calc {
c := Calc{stack: NewStack(), debug: false}
c.completer = readline.NewPrefixCompleter(
// custom lua functions
readline.PcItemDynamic(GetCompleteCustomFunctions()),
// commands
readline.PcItem("dump"),
readline.PcItem("reverse"),
readline.PcItem("debug"),
readline.PcItem("clear"),
readline.PcItem("batch"),
readline.PcItem("shift"),
readline.PcItem("undo"),
readline.PcItem("help"),
readline.PcItem("history"),
// ops
readline.PcItem("+"),
readline.PcItem("-"),
readline.PcItem("*"),
readline.PcItem("/"),
readline.PcItem("^"),
readline.PcItem("%"),
readline.PcItem("%-"),
readline.PcItem("%+"),
// constants
readline.PcItem("Pi"),
readline.PcItem("Phi"),
readline.PcItem("Sqrt2"),
readline.PcItem("SqrtE"),
readline.PcItem("SqrtPi"),
readline.PcItem("SqrtPhi"),
readline.PcItem("Ln2"),
readline.PcItem("Log2E"),
readline.PcItem("Ln10"),
readline.PcItem("Log10E"),
// math functions
readline.PcItem("sqrt"),
readline.PcItem("remainder"),
readline.PcItem("avg"),
readline.PcItem("median"),
)
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(`^[\+\-\*\/]$`)
constants := []string{"E", "Pi", "Phi", "Sqrt2", "SqrtE", "SqrtPi",
"SqrtPhi", "Ln2", "Log2E", "Ln10", "Log10E"}
functions := []string{"sqrt", "remainder", "%", "%-", "%+"}
batch := []string{"median", "avg"}
luafuncs := []string{}
if line == "" {
return
}
for luafunc := range LuaFuncs {
luafuncs = append(luafuncs, luafunc)
}
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
}
if contains(constants, item) {
c.stack.Backup()
c.stack.Push(const2num(item))
continue
}
if contains(functions, item) {
c.mathfunc(item)
continue
}
if contains(batch, item) {
c.batchfunc(item)
continue
}
if contains(luafuncs, item) {
c.luafunc(item)
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 "reverse":
c.stack.Backup()
c.stack.Reverse()
case "undo":
c.stack.Restore()
case "history":
for _, entry := range c.history {
fmt.Println(entry)
}
case "^":
c.exp()
default:
fmt.Println("unknown command or operator!")
}
}
}
}
func (c *Calc) History(format string, args ...any) {
c.history = append(c.history, fmt.Sprintf(format, args...))
}
func (c *Calc) Result() float64 {
if !c.stdin {
fmt.Print("= ")
}
fmt.Println(c.stack.Last())
return 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)
c.History("%f %c %f = %f", a, op, b, x)
if !c.batch {
break
}
}
_ = c.Result()
}
func (c *Calc) exp() {
c.stack.Backup()
for c.stack.Len() > 1 {
b := c.stack.Pop()
a := c.stack.Pop()
x := math.Pow(a, b)
c.stack.Push(x)
c.History("%f ^ %f = %f", a, b, x)
if !c.batch {
break
}
}
_ = c.Result()
}
func (c *Calc) mathfunc(funcname string) {
// FIXME: split into 2 funcs, one working with 1 the other with 2
// args, saving Pop calls
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 "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()
}
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 "avg":
var sum float64
for c.stack.Len() > 0 {
sum += c.stack.Pop()
}
x = sum / float64(count)
c.History("avg(all)")
}
c.stack.Push(x)
_ = c.Result()
}
func (c *Calc) luafunc(funcname string) {
// we may need to put them onto the stack afterwards!
c.stack.Backup()
b := c.stack.Pop()
a := c.stack.Pop()
x, err := CallLuaFunc(c.L, funcname, a, b)
if err != nil {
fmt.Println(err)
c.stack.Push(a)
c.stack.Push(b)
return
}
c.History("%s(%f,%f) = %f", funcname, a, b, x)
c.stack.Push(x)
c.Result()
}