diff --git a/djpianalto.com/goff/exts/fun.go b/djpianalto.com/goff/exts/fun.go index 3471b09..5084128 100644 --- a/djpianalto.com/goff/exts/fun.go +++ b/djpianalto.com/goff/exts/fun.go @@ -2,8 +2,11 @@ package exts import ( "fmt" - "github.com/dustinpianalto/disgoman" "strconv" + "strings" + + "github.com/dustinpianalto/disgoman" + "github.com/dustinpianalto/rpnparse" ) func interleave(ctx disgoman.Context, args []string) { @@ -47,3 +50,35 @@ func deinterleave(ctx disgoman.Context, args []string) { ctx.Send(fmt.Sprintf("(%v, %v)", x, y)) } } + +func generateRPNCommand(ctx disgoman.Context, args []string) { + rpn, err := rpnparse.GenerateRPN(args) + if err != nil { + ctx.Send(err.Error()) + return + } + ctx.Send(rpn) +} + +func parseRPNCommand(ctx disgoman.Context, args []string) { + res, err := rpnparse.ParseRPN(args) + if err != nil { + ctx.Send(err.Error()) + return + } + ctx.Send(fmt.Sprintf("The result is: %v", res)) +} + +func solveCommand(ctx disgoman.Context, args []string) { + rpn, err := rpnparse.GenerateRPN(args) + if err != nil { + ctx.Send(err.Error()) + return + } + res, err := rpnparse.ParseRPN(strings.Split(rpn, " ")) + if err != nil { + ctx.Send(err.Error()) + return + } + ctx.Send(fmt.Sprintf("The result is: %v", res)) +} diff --git a/djpianalto.com/goff/exts/init.go b/djpianalto.com/goff/exts/init.go index 9f7471f..316b3b7 100644 --- a/djpianalto.com/goff/exts/init.go +++ b/djpianalto.com/goff/exts/init.go @@ -204,4 +204,30 @@ func AddCommandHandlers(h *disgoman.CommandManager) { RequiredPermissions: disgoman.PermissionManageServer, Invoke: getPuzzleChannel, }) + Name: "RPN", + Aliases: []string{"rpn"}, + Description: "Convert infix to rpn", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: generateRPNCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "ParseRPN", + Aliases: []string{"PRPN", "prpn"}, + Description: "Parse RPN string and return the result", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: parseRPNCommand, + }) + _ = h.AddCommand(&disgoman.Command{ + Name: "solve", + Aliases: []string{"math", "infix"}, + Description: "Solve infix equation and return the result", + OwnerOnly: false, + Hidden: false, + RequiredPermissions: 0, + Invoke: solveCommand, + }) } diff --git a/djpianalto.com/goff/utils/rpn.go b/djpianalto.com/goff/utils/rpn.go new file mode 100644 index 0000000..db39f5d --- /dev/null +++ b/djpianalto.com/goff/utils/rpn.go @@ -0,0 +1,156 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" +) + +type Operator struct { + Token string + Precedence int + Association string +} + +func (o Operator) HasHigherPrecedence(t Operator) bool { + return o.Precedence < t.Precedence // lower number is higher precedence +} + +func (o Operator) HasEqualPrecedence(t Operator) bool { + return o.Precedence == t.Precedence +} + +func (o Operator) IsLeftAssociative() bool { + return o.Association == "left" +} + +var operators = map[string]Operator{ + "+": Operator{ + Token: "+", + Precedence: 4, + Association: "left", + }, + "-": Operator{ + Token: "-", + Precedence: 4, + Association: "left", + }, + "*": Operator{ + Token: "*", + Precedence: 3, + Association: "left", + }, + "/": Operator{ + Token: "/", + Precedence: 3, + Association: "left", + }, + "%": Operator{ + Token: "%", + Precedence: 3, + Association: "left", + }, + "(": Operator{ + Token: "(", + Precedence: 1, + Association: "left", + }, + ")": Operator{ + Token: ")", + Precedence: 1, + Association: "left", + }, +} + +type Stack []Operator + +func (s *Stack) IsEmpty() bool { + return len(*s) == 0 +} + +func (s *Stack) Push(op Operator) { + *s = append(*s, op) +} + +func (s *Stack) Pop() (Operator, bool) { + if s.IsEmpty() { + return Operator{}, false + } + index := len(*s) - 1 + element := (*s)[index] + *s = (*s)[:index] + return element, true +} + +func (s *Stack) Top() Operator { + if s.IsEmpty() { + return Operator{} + } + return (*s)[len(*s)-1] +} + +func GenerateRPN(tokens []string) (string, error) { + output := "" + s := Stack{} + for _, token := range tokens { + err := processToken(token, &s, &output) + if err != nil { + return "", err + } + } + for !s.IsEmpty() { + ele, _ := s.Pop() + output += " " + ele.Token + } + + return strings.TrimSpace(output), nil +} + +func processToken(t string, s *Stack, o *string) error { + if _, err := strconv.Atoi(t); err == nil { + *o += " " + t + return nil + } else if op, ok := operators[t]; ok { + if op.Token == "(" { + s.Push(op) + } else if op.Token == ")" { + if s.IsEmpty() { + return fmt.Errorf("mismatched parentheses") + } + for s.Top().Token != "(" { + if ele, ok := s.Pop(); ok { + *o += " " + ele.Token + } else { + return fmt.Errorf("mismatched parentheses") + } + if s.IsEmpty() { + break + } + } + s.Pop() // Pop and discard the ( + } else if !s.IsEmpty() { + for { + if (s.Top().HasHigherPrecedence(op) || + (s.Top().HasEqualPrecedence(op) && + op.IsLeftAssociative())) && + s.Top().Token != "(" { + if ele, ok := s.Pop(); ok { + *o += " " + ele.Token + if s.IsEmpty() { + break + } + continue + } else { + break + } + } + break + } + s.Push(op) + } else { + s.Push(op) + } + return nil + } + return fmt.Errorf("invalid character %s", t) +} diff --git a/djpianalto.com/goff/utils/rpnParser.go b/djpianalto.com/goff/utils/rpnParser.go new file mode 100644 index 0000000..7f9dff9 --- /dev/null +++ b/djpianalto.com/goff/utils/rpnParser.go @@ -0,0 +1,95 @@ +package utils + +import ( + "errors" + "fmt" + "math" + "strconv" +) + +type FStack []float64 + +func (s *FStack) IsEmpty() bool { + return len(*s) == 0 +} + +func (s *FStack) Push(op float64) { + *s = append(*s, op) +} + +func (s *FStack) Pop() (float64, bool) { + if s.IsEmpty() { + return 0, false + } + index := len(*s) - 1 + element := (*s)[index] + *s = (*s)[:index] + return element, true +} + +func (s *FStack) PopTwo() (float64, float64, bool) { + if s.IsEmpty() || len(*s) < 2 { + return 0, 0, false + } + index := len(*s) - 1 + b := (*s)[index] + a := (*s)[index-1] + *s = (*s)[:index-1] + return a, b, true + +} + +func (s *FStack) Top() float64 { + if s.IsEmpty() { + return 0 + } + return (*s)[len(*s)-1] +} + +func ParseRPN(args []string) (float64, error) { + s := FStack{} + for _, token := range args { + switch token { + case "+": + if a, b, ok := s.PopTwo(); ok { + s.Push(a + b) + } else { + return 0, fmt.Errorf("not enough operands on stack for +: %v", s) + } + case "-": + if a, b, ok := s.PopTwo(); ok { + s.Push(a - b) + } else { + return 0, fmt.Errorf("not enough operands on stack for -: %v", s) + } + case "*": + if a, b, ok := s.PopTwo(); ok { + s.Push(a * b) + } else { + return 0, fmt.Errorf("not enough operands on stack for *: %v", s) + } + case "/": + if a, b, ok := s.PopTwo(); ok { + s.Push(a / b) + } else { + return 0, fmt.Errorf("not enough operands on stack for /: %v", s) + } + case "%": + if a, b, ok := s.PopTwo(); ok { + s.Push(math.Mod(a, b)) + } else { + return 0, fmt.Errorf("not enough operands on stack for %: %v", s) + } + default: + f, err := strconv.ParseFloat(token, 64) + if err != nil { + return 0, err + } + s.Push(f) + } + } + if res, ok := s.Pop(); ok { + return res, nil + } + return 0, errors.New("no result") +} diff --git a/go.mod b/go.mod index 22156f7..29d07d6 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/dustinpianalto/disgoman v0.0.10 github.com/emersion/go-imap v1.0.5 github.com/emersion/go-message v0.12.0 + github.com/dustinpianalto/rpnparse v1.0.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/lib/pq v1.3.0 github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254