-
Notifications
You must be signed in to change notification settings - Fork 406
feat(example): calculator realm #4084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 19 commits
9b9492a
354b409
69c4861
e330d9c
345cc21
8cd3d2a
44cb487
f7ebebc
c7b7ad9
7787495
9aaa711
ffeae14
def6b5c
09209d1
c566b19
e10b516
7df0d90
d529ff5
8a1f4e7
c34cbf4
99fd13f
2749424
d216360
ffeda4b
e2ddb3c
cda0688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
package calculator | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
|
||
"gno.land/p/moul/md" | ||
"gno.land/p/moul/mdtable" | ||
"gno.land/p/moul/realmpath" | ||
"gno.land/r/leon/hor" | ||
) | ||
|
||
type Node struct { | ||
value string // Value of the current node | ||
left *Node | ||
right *Node | ||
} | ||
|
||
const ( | ||
specialCharacters = "p-*/." | ||
specialCharactersWithoutMinus = "p*/." | ||
topPriority = "*/" | ||
lowPriority = "p-" | ||
|
||
realmPath = "/r/miko/calculator" | ||
) | ||
|
||
var ( | ||
val float64 | ||
displayVal string | ||
|
||
operationMap = map[string]func(left float64, right float64) float64{ | ||
"p": func(left float64, right float64) float64 { return left + right }, | ||
"-": func(left float64, right float64) float64 { return left - right }, | ||
"*": func(left float64, right float64) float64 { return left * right }, | ||
"/": func(left float64, right float64) float64 { | ||
if right == 0 { | ||
panic("Division by 0 is forbidden") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 0 division can only be detected when reading the tree, and I don't see a way of properly handling this case that doesn't involve a weird third parameter for all operation function, or a global variable |
||
} | ||
return left / right | ||
}, | ||
} | ||
) | ||
|
||
func init() { | ||
hor.Register("Miko's calculator", "Let's do maths") | ||
} | ||
|
||
func evaluateValidity(line string) (bool, string) { | ||
if len(line) == 0 { | ||
return false, "Invalid empty input" | ||
} // edge case empty line | ||
if strings.Index(specialCharactersWithoutMinus, string(line[0])) != -1 || | ||
strings.Index(specialCharacters, string(line[len(line)-1])) != -1 { | ||
return false, "Invalid equation" | ||
} // edge case special character at begining or end | ||
|
||
isPriorSpecial := false | ||
countParenthesis := 0 | ||
|
||
for i := 0; i < len(line); i++ { | ||
if line[i] == '<' { | ||
countParenthesis += 1 | ||
continue | ||
} | ||
if line[i] == '>' { | ||
if isPriorSpecial == true { | ||
return false, "Invalid equation" | ||
} | ||
countParenthesis -= 1 | ||
isPriorSpecial = false | ||
continue | ||
} | ||
if strings.Index(specialCharacters, string(line[i])) != -1 { | ||
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) | ||
return false, "Invalid equation" | ||
} | ||
isPriorSpecial = true | ||
continue | ||
} | ||
if line[i] != 'p' && (line[i] < '0' || line[i] > '9') { | ||
return false, "Invalid character encountered " | ||
} | ||
isPriorSpecial = false | ||
} | ||
|
||
if countParenthesis != 0 { | ||
return false, "Invalid equation" | ||
} | ||
return true, "" | ||
} | ||
|
||
func searchForPriority(priorityList string, line string) *Node { | ||
countParenthesis := 0 | ||
for iPrio := 0; iPrio < len(priorityList); iPrio++ { | ||
for idx := 0; idx < len(line); idx++ { | ||
if line[idx] == '<' { | ||
countParenthesis += 1 | ||
} | ||
if line[idx] == '>' { | ||
countParenthesis -= 1 | ||
} | ||
if countParenthesis == 0 && line[idx] == priorityList[iPrio] && | ||
!(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 | ||
return &Node{string(line[idx]), createTree(line[:idx]), createTree(line[idx+1:])} | ||
} | ||
|
||
} | ||
} | ||
return nil | ||
} | ||
|
||
// checks if the expression in line is contained in one big parenthesis | ||
func isInOneParenthesis(line string) bool { | ||
if line[0] != '<' || line[len(line)-1] != '>' { | ||
return false | ||
} | ||
countParenthesis := 1 | ||
for i := 1; i < len(line)-1; i++ { | ||
if line[i] == '<' { | ||
countParenthesis += 1 | ||
} | ||
if line[i] == '>' { | ||
countParenthesis -= 1 | ||
} | ||
if countParenthesis == 0 { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
func createTree(line string) *Node { | ||
if isInOneParenthesis(line) { | ||
return createTree(line[1 : len(line)-1]) | ||
} | ||
node := searchForPriority(lowPriority, line) // we put the lowest priority at the top of the tree, these operations will be executed last | ||
if node != nil { | ||
return node | ||
} | ||
node = searchForPriority(topPriority, line) | ||
if node != nil { | ||
return node | ||
} | ||
|
||
// if this code is reached, the only value possible in line is a number | ||
return &Node{line, nil, nil} | ||
} | ||
|
||
func readTree(tree *Node) float64 { | ||
operation, exists := operationMap[tree.value] | ||
|
||
if exists { // check if the current node is an operator | ||
return operation(readTree(tree.left), readTree(tree.right)) | ||
} | ||
|
||
parsedValue, _ := strconv.ParseFloat(tree.value, 64) | ||
|
||
return parsedValue | ||
} | ||
|
||
// expression is the equation you want to solve (p replaces the + symbol) | ||
// exemple: 2p4/2 | ||
func ComputeResult(expression string) string { | ||
valid, errString := evaluateValidity(expression) | ||
|
||
if !valid { // If a basic error is encountered, return the expression without the = at the end and display the same expression | ||
println(errString) // display error for debug | ||
displayVal = strings.Replace(expression, "p", "+", -1) | ||
displayVal = strings.Replace(displayVal, "<", "(", -1) | ||
displayVal = strings.Replace(displayVal, ">", ")", -1) | ||
return expression | ||
} | ||
|
||
tree := createTree(expression) | ||
|
||
val = readTree(tree) | ||
displayVal = strconv.FormatFloat(val, 'g', 6, 64) | ||
return displayVal | ||
} | ||
|
||
func removeLast(path string) string { | ||
lenPath := len(path) | ||
if lenPath > 0 { | ||
path = path[:lenPath-1] | ||
} | ||
return path | ||
} | ||
|
||
func Render(path string) string { | ||
|
||
req := realmpath.Parse(path) | ||
query := req.Query | ||
expression := query.Get("expression") | ||
|
||
if expression == "" { | ||
displayVal = "0" | ||
} else { | ||
if expression[len(expression)-1] == '=' { | ||
expression = ComputeResult(expression[:len(expression)-1]) | ||
} else { | ||
displayVal = strings.Replace(expression, "p", "+", -1) | ||
displayVal = strings.Replace(displayVal, "<", "(", -1) | ||
displayVal = strings.Replace(displayVal, ">", ")", -1) | ||
} | ||
} | ||
|
||
out := md.H1("Calculator page") | ||
out += md.H3("Have you ever wanted to do maths but never actually found a calculator ?") | ||
out += md.H3("Do I have the realm for you...") | ||
out += "---------------\n" | ||
|
||
out += md.H2("Result: " + displayVal) | ||
table := mdtable.Table{ | ||
Headers: []string{md.Link("res", realmPath), md.Link("(", realmPath+":?expression="+expression+"<"), md.Link(")", realmPath+":?expression="+expression+">"), md.Link("del", realmPath+":?expression="+removeLast(expression))}, | ||
} | ||
table.Append([]string{md.Link("7", realmPath+":?expression="+expression+"7"), md.Link("8", realmPath+":?expression="+expression+"8"), md.Link("9", realmPath+":?expression="+expression+"9"), md.Link("+", realmPath+":?expression="+expression+"p")}) | ||
table.Append([]string{md.Link("4", realmPath+":?expression="+expression+"4"), md.Link("5", realmPath+":?expression="+expression+"5"), md.Link("6", realmPath+":?expression="+expression+"6"), md.Link("-", realmPath+":?expression="+expression+"-")}) | ||
table.Append([]string{md.Link("1", realmPath+":?expression="+expression+"1"), md.Link("2", realmPath+":?expression="+expression+"2"), md.Link("3", realmPath+":?expression="+expression+"3"), md.Link("*", realmPath+":?expression="+expression+"*")}) | ||
table.Append([]string{md.Link("0", realmPath+":?expression="+expression+"0"), md.Link(".", realmPath+":?expression="+expression+"."), md.Link("=", realmPath+":?expression="+expression+"="), md.Link("/", realmPath+":?expression="+expression+"/")}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. line = make([]string, 0, 3)
for idx, c := range "7894561230.=/" {
query.Set("expression", expression + c)
line = append(line, md.Link(string(c), req.String()))
if len(line) == 3 {
table.Append(line)
line = line[:0]
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to change the way line is reset here. I'm not 100% confident in gno or go yet, but it seemed like resetting it this way only reseted the index of the []string like a pointer, making it so that every line had the same few characters (. = / here) is this a good way to handle this situation in gno ? |
||
out += table.String() | ||
|
||
return out | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package calculator | ||
|
||
import "testing" | ||
|
||
func Test_Addition(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("1p1") | ||
// Verify the value is equal to 2 | ||
if value != "2" { | ||
t.Fatalf("1 + 1 is not equal to 2") | ||
} | ||
} | ||
|
||
func Test_Subtraction(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("1-1") | ||
// Verify the value is equal to 0 | ||
if value != "0" { | ||
t.Fatalf("1 - 1 is not equal to 0") | ||
} | ||
} | ||
|
||
func Test_Multiplication(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("1*4") | ||
// Verify the value is equal to 4 | ||
if value != "4" { | ||
t.Fatalf("1 * 4 is not equal to 4") | ||
} | ||
} | ||
|
||
func Test_Division(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("4/2") | ||
// Verify the value is equal to 2 | ||
if value != "2" { | ||
t.Fatalf("4 / 2 is not equal to 2") | ||
} | ||
} | ||
|
||
func Test_AdditionDecimal(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("1.2p1.3") | ||
// Verify the value is equal to 2.5 | ||
if value != "2.5" { | ||
t.Fatalf("1.2 + 1.3 is not equal to 2.5") | ||
} | ||
} | ||
|
||
func Test_SubtractionDecimal(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("1.3-1.2") | ||
// Verify the value is equal to 0.1 | ||
if value != "0.1" { | ||
t.Fatalf("1.3 - 1.2 is not equal to 0.1") | ||
} | ||
} | ||
|
||
func Test_MultiplicationDecimal(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("3*1.5") | ||
// Verify the value is equal to 4.5 | ||
if value != "4.5" { | ||
t.Fatalf("3 * 1.5 is not equal to 4.5") | ||
} | ||
} | ||
|
||
func Test_DivisionDecimal(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("2/0.5") | ||
// Verify the value is equal to 4 | ||
if value != "4" { | ||
t.Fatalf("2 / 0.5 is not equal to 4") | ||
} | ||
} | ||
|
||
func Test_BaseParenthesis(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("2*<3p4>") | ||
// Verify the value is equal to 14 | ||
if value != "14" { | ||
t.Fatalf("2 * (3 + 4) is not equal to 14") | ||
} | ||
} | ||
|
||
func Test_AdvancedParenthesis(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("2*<3p4/<6-2>>") | ||
// Verify the value is equal to 8 | ||
if value != "8" { | ||
t.Fatalf("2 * (3 + 4 / (6 - 2)) is not equal to 8") | ||
} | ||
} | ||
|
||
func Test_Negative(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("-2p10") | ||
// Verify the value is equal to 8 | ||
if value != "8" { | ||
t.Fatalf("-2 + 10 is not equal to 8") | ||
} | ||
} | ||
|
||
func Test_Negative2(t *testing.T) { | ||
// Increment the value | ||
value := ComputeResult("-2*-10") | ||
// Verify the value is equal to 20 | ||
if value != "20" { | ||
t.Fatalf("-2 * -10 is not equal to 20") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/r/miko/calculator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not
+
? (it should be fine as long as it's url-encoded)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like
query.Get("expression")
removes the '+'
I have tried printing the path given by Render and the expression obtained via query.Get
path : ?expression=4+
expression : 4
Still the solution of using req.String allowed me to use '(' and ')' instead of '<' and '>' !
c34cbf4