I'm pretty new to Go, and I do not know the best or idiomatic ways to do most of the stuff, so I'd appreciate feedback on how to make my code better, faster or more idiomatic.
My program is a set of methods/functions to parse an infix notation expression then convert it to reverse polish notation using Dijkstra's Shunting-Yard algorithm and then finally solve it, so far it supports basic operations "+ - / * ^", nested parentheses & basic trig functions.
File 1:
package gocalc
import (
"strings"
"unicode"
)
type Token struct {
Type TokenType
Value string
}
type TokenType int
const (
Number TokenType = iota
Operator
LParen
RParen
fnction
)
// fnction Parse converts an expression into a stack of tokens
func Parse(input string) (tokens Stack) {
var cont string
var fn bool
for _, v := range input {
val := string(v)
if unicode.IsDigit(v) || val == "." {
cont += val
} else if unicode.IsLetter(v) || strings.Contains("[]", val) {
if val == "]" {
cont += val
fn = false
} else {
cont += val
fn = true
}
} else if strings.Contains("+-*/^", val) {
if fn {
cont += val
} else {
if cont != "" {
if unicode.IsLetter(rune(cont[0])) {
tokens.Push(Token{fnction, cont})
} else {
tokens.Push(Token{Number, cont})
}
cont = ""
}
tokens.Push(Token{Operator, val})
}
} else if strings.Contains("()", val) {
if fn {
cont += val
} else {
if cont != "" {
if unicode.IsLetter(rune(cont[0])) {
tokens.Push(Token{fnction, cont})
} else {
tokens.Push(Token{Number, cont})
}
cont = ""
}
if val == "(" {
tokens.Push(Token{LParen, val})
} else {
tokens.Push(Token{RParen, val})
}
}
}
}
if cont != "" {
if unicode.IsLetter(rune(cont[0])) {
tokens.Push(Token{fnction, cont})
} else {
tokens.Push(Token{Number, cont})
}
}
return tokens
}
File 2:
package gocalc
import (
"math"
"strconv"
"strings"
)
var oprData = map[string]struct {
prec int
asoc bool // true = right // false = left
fx func(x, y float64) float64
}{
"^": {4, true, func(x, y float64) float64 { return math.Pow(x, y) }},
"*": {3, false, func(x, y float64) float64 { return x * y }},
"/": {3, false, func(x, y float64) float64 { return x / y }},
"+": {2, false, func(x, y float64) float64 { return x + y }},
"-": {2, false, func(x, y float64) float64 { return x - y }},
}
var fnData = map[string]func(x float64) float64{
"Cos": func(x float64) float64 { return math.Cos(x) },
"Sin": func(x float64) float64 { return math.Sin(x) },
"Tan": func(x float64) float64 { return math.Tan(x) },
"Acos": func(x float64) float64 { return math.Acos(x) },
"Asin": func(x float64) float64 { return math.Asin(x) },
"Atan": func(x float64) float64 { return math.Atan(x) },
}
// Function ToRPN converts a stack of tokens from infix notation to Reverse Polish Notation
func ToRPN(tokens Stack) Stack {
output := Stack{}
ops := Stack{}
for _, v := range tokens.Values {
switch {
case v.Type == Operator:
for !ops.IsEmpty() {
val := v.Value
top := ops.Peek().Value
if (oprData[val].prec <= oprData[top].prec && oprData[val].asoc == false) ||
(oprData[val].prec < oprData[top].prec && oprData[val].asoc == true) {
output.Push(ops.Pop())
continue
}
break
}
ops.Push(v)
case v.Type == LParen:
ops.Push(v)
case v.Type == RParen:
for i := ops.Length() - 1; i >= 0; i-- {
if ops.Values[i].Type != LParen {
output.Push(ops.Pop())
continue
} else {
ops.Pop()
break
}
}
default:
output.Push(v)
}
}
if !ops.IsEmpty() {
for i := ops.Length() - 1; i >= 0; i-- {
output.Push(ops.Pop())
}
}
return output
}
// Function SolveRPN returns the result of the Reverse Polish Notation expression
func SolveRPN(tokens Stack) float64 {
stack := Stack{}
for _, v := range tokens.Values {
if v.Type == Number {
stack.Push(v)
} else if v.Type == Function {
stack.Push(Token{Number, SolveFunction(v.Value)})
} else if v.Type == Operator {
f := oprData[v.Value].fx
var x, y float64
y, _ = strconv.ParseFloat(stack.Pop().Value, 64)
x, _ = strconv.ParseFloat(stack.Pop().Value, 64)
result := f(x, y)
stack.Push(Token{Number, strconv.FormatFloat(result, 'f', 12, 64)})
}
}
out, _ := strconv.ParseFloat(stack.Values[0].Value, 64)
return out
}
func SolveFunction(input string) string {
var fArg float64
fType := strings.TrimRight(input, "[.]0123456789+-/*^()")
args := strings.TrimLeft(strings.TrimRight(input, "]"), fType+"[")
if !strings.ContainsAny(args, "+ & * & - & / & ^") {
fArg, _ = strconv.ParseFloat(args, 64)
} else {
stack := Parse(args)
stack = ToRPN(stack)
fArg = SolveRPN(stack)
}
return strconv.FormatFloat(fnData[fType](fArg), 'f', 12, 64)
}
File 3:
package gocalc
type Stack struct {
Values []Token
}
// Function Pop removes the top token of the stack and returns its value
func (stack *Stack) Pop() (i Token) {
if len(stack.Values) == 0 {
return
}
i = stack.Values[len(stack.Values)-1]
stack.Values = stack.Values[:len(stack.Values)-1]
return
}
// Fuction Push adds a token to the top of the stack
func (stack *Stack) Push(i ...Token) {
stack.Values = append(stack.Values, i...)
}
// Function Peek returns the token at the top of the stack
func (stack *Stack) Peek() Token {
return stack.Values[len(stack.Values)-1]
}
// Function IsEmpty check if there are any tokens in the stack
func (stack *Stack) IsEmpty() bool {
return len(stack.Values) == 0
}
// Function Length returns the amount of tokens in the stack
func (stack *Stack) Length() int {
return len(stack.Values)
}
The code can be found on GitHub.
I plan to further develop the code and to eventually add equation solving and more advanced features.