calculator.gno

6.01 Kb ยท 233 lines
  1package calculator
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/moul/md"
  9	"gno.land/p/moul/mdtable"
 10	"gno.land/p/moul/realmpath"
 11	"gno.land/r/leon/hor"
 12)
 13
 14type Node struct {
 15	value string // Value of the current node
 16	left  *Node
 17	right *Node
 18}
 19
 20const (
 21	specialCharacters             = "+-*/."
 22	specialCharactersWithoutMinus = "+*/."
 23	topPriority                   = "*/"
 24	lowPriority                   = "+-"
 25	realmPath                     = "/r/miko/calculator"
 26)
 27
 28var (
 29	val        float64
 30	displayVal string
 31
 32	operationMap = map[string]func(left float64, right float64) float64{
 33		"+": func(left float64, right float64) float64 { return left + right },
 34		"-": func(left float64, right float64) float64 { return left - right },
 35		"*": func(left float64, right float64) float64 { return left * right },
 36		"/": func(left float64, right float64) float64 {
 37			if right == 0 {
 38				panic("Division by 0 is forbidden")
 39			}
 40			return left / right
 41		},
 42	}
 43)
 44
 45func init() {
 46	hor.Register(cross, "Miko's calculator", "")
 47}
 48
 49func evaluateValidity(line string) (bool, string) {
 50	if len(line) == 0 {
 51		return false, "Invalid empty input"
 52	} // edge case empty line
 53	if strings.Index(specialCharactersWithoutMinus, string(line[0])) != -1 ||
 54		strings.Index(specialCharacters, string(line[len(line)-1])) != -1 {
 55		return false, "Invalid equation"
 56	} // edge case special character at begining or end
 57
 58	isPriorSpecial := false
 59	countParenthesis := 0
 60
 61	for i := 0; i < len(line); i++ {
 62		if line[i] == '(' {
 63			countParenthesis += 1
 64			continue
 65		}
 66		if line[i] == ')' {
 67			if isPriorSpecial == true {
 68				return false, "Invalid equation"
 69			}
 70			countParenthesis -= 1
 71			isPriorSpecial = false
 72			continue
 73		}
 74		if strings.Index(specialCharacters, string(line[i])) != -1 {
 75			if isPriorSpecial && !(line[i] == '-' && i < (len(line)-1) && line[i+1] >= '0' && line[i+1] <= '9') { // if we have two subsequent operator and the second one isn't a - before a number (negative number)
 76				return false, "Invalid equation"
 77			}
 78			isPriorSpecial = true
 79			continue
 80		}
 81		if line[i] < '0' || line[i] > '9' {
 82			return false, "Invalid character encountered "
 83		}
 84		isPriorSpecial = false
 85	}
 86
 87	if countParenthesis != 0 {
 88		return false, "Invalid equation"
 89	}
 90	return true, ""
 91}
 92
 93func searchForPriority(priorityList string, line string) *Node {
 94	countParenthesis := 0
 95	for iPrio := 0; iPrio < len(priorityList); iPrio++ {
 96		for idx := 0; idx < len(line); idx++ {
 97			if line[idx] == '(' {
 98				countParenthesis += 1
 99			}
100			if line[idx] == ')' {
101				countParenthesis -= 1
102			}
103			if countParenthesis == 0 && line[idx] == priorityList[iPrio] &&
104				!(line[idx] == '-' && (idx == 0 || strings.Index(specialCharacters, string(line[idx-1])) != -1)) { // - is not a substract sign if at the begining or after another sign
105				return &Node{string(line[idx]), createTree(line[:idx]), createTree(line[idx+1:])}
106			}
107
108		}
109	}
110	return nil
111}
112
113// checks if the expression in line is contained in one big parenthesis
114func isInOneParenthesis(line string) bool {
115	if line[0] != '(' || line[len(line)-1] != ')' {
116		return false
117	}
118	countParenthesis := 1
119	for i := 1; i < len(line)-1; i++ {
120		if line[i] == '(' {
121			countParenthesis += 1
122		}
123		if line[i] == ')' {
124			countParenthesis -= 1
125		}
126		if countParenthesis == 0 {
127			return false
128		}
129	}
130	return true
131}
132
133func createTree(line string) *Node {
134	if isInOneParenthesis(line) {
135		return createTree(line[1 : len(line)-1])
136	}
137	node := searchForPriority(lowPriority, line) // we put the lowest priority at the top of the tree, these operations will be executed last
138	if node != nil {
139		return node
140	}
141	node = searchForPriority(topPriority, line)
142	if node != nil {
143		return node
144	}
145
146	// if this code is reached, the only value possible in line is a number
147	return &Node{line, nil, nil}
148}
149
150func readTree(tree *Node) float64 {
151	operation, exists := operationMap[tree.value]
152
153	if exists { // check if the current node is an operator
154		return operation(readTree(tree.left), readTree(tree.right))
155	}
156
157	parsedValue, _ := strconv.ParseFloat(tree.value, 64)
158
159	return parsedValue
160}
161
162// expression is the equation you want to solve (p replaces the + symbol)
163// exemple: 2p4/2
164func ComputeResult(expression string) string {
165	valid, errString := evaluateValidity(expression)
166
167	if !valid { // If a basic error is encountered, return the expression without the = at the end and display the same expression
168		println(errString) // display error for debug
169		return expression
170	}
171	tree := createTree(expression)
172	val = readTree(tree)
173	displayVal = strconv.FormatFloat(val, 'g', 6, 64)
174	return displayVal
175}
176
177func removeLast(path string) string {
178	lenPath := len(path)
179	if lenPath > 0 {
180		path = path[:lenPath-1]
181	}
182	return path
183}
184
185func createTable(req *realmpath.Request, query url.Values, expression string) mdtable.Table {
186	line := make([]string, 0, 4)
187	query.Set("expression", "")
188	line = append(line, md.Link("res", req.String()))
189	for _, str := range []string{"(", ")"} {
190		query.Set("expression", expression+str)
191		line = append(line, md.Link(str, req.String()))
192	}
193	query.Set("expression", removeLast(expression))
194	line = append(line, md.Link("del", req.String())) // req and del are two special cases
195	table := mdtable.Table{
196		Headers: line,
197	}
198	line = []string{}
199	for _, c := range "789+456-123*0.=/" {
200		query.Set("expression", expression+string(c))
201		line = append(line, md.Link(string(c), req.String()))
202		if len(line) == 4 {
203			table.Append(line)
204			line = []string{}
205		}
206	}
207	return table
208}
209
210func Render(path string) string {
211	req := realmpath.Parse(path)
212	query := req.Query
213	expression := query.Get("expression")
214
215	if expression == "" {
216		displayVal = "0"
217	} else {
218		if expression[len(expression)-1] == '=' {
219			expression = ComputeResult(expression[:len(expression)-1])
220		} else {
221			displayVal = expression
222		}
223	}
224	out := md.H1("Calculator page")
225	out += md.H3("Have you ever wanted to do maths but never actually found a calculator ?")
226	out += md.H3("Do I have the realm for you...")
227	out += "---------------\n"
228	out += md.H2("Result: " + displayVal)
229
230	table := createTable(req, query, expression)
231	out += table.String()
232	return out
233}