Skip to content

Instantly share code, notes, and snippets.

@Yardanico
Created June 10, 2018 10:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Yardanico/5c08e1c3b9a7ad38b6edace91ba96cce to your computer and use it in GitHub Desktop.
Save Yardanico/5c08e1c3b9a7ad38b6edace91ba96cce to your computer and use it in GitHub Desktop.
import math, strutils, parseutils, strformat
type
MathFunction = proc(args: seq[float]): float
NodeKind = enum
Num, Mul, Div, Mod, Pow, Add, Sub, UnarySub, UnaryAdd, Fun, Var
Node = ref object
case kind: NodeKind
of Num: ## Number
val: float
of Mul, Div, Mod, Pow, Add, Sub:
left, right: Node
of UnarySub, UnaryAdd:
unaryNode: Node
of Fun:
funName: string
args: seq[Node]
of Var:
varName: string
const
ArgsErrorMsg = "Expected $1 arguments for function `$2`, got $3"
AtLeastErrorMsg = "Function `$1` accepts at least one argument, got 0"
CharErrorMsg = "Unexpected char $1 at pos $2"
UnknownIdentMsg = "Unknown function, variable or constant `$1` at pos $2"
proc `$`(n: Node): string =
case n.kind
of Num: &"Node(value: {n.val})"
of Mul, Div, Mod, Pow, Add, Sub:
&"Node(op: {n.kind}, left: {n.left}, right: {n.right})"
of UnaryAdd, UnarySub:
&"Node(unaryOp: {n.kind}, node: {n.unaryNode})"
of Fun:
&"Node(name: {n.funName}, args: {n.args})"
of Var:
&"Node(var: {n.varName})"
# TODO: Make a PR to Nim stdlib
when defined(JS):
proc `mod`(a, b: float): float =
asm """`a` % `b`"""
proc parse*(data: string): Node =
## Evaluates math Node from string *data* and returns result as a float
##
## Has optional *vars* argument - table of variables which can be used inside
## of a math Node
let
maxPos = data.len
var
pos = 0 ## Current position
ch = data[0] ## Current character
template nextChar =
## Increments current position and gets next char
inc pos
# Check if string ended
if pos == maxPos: ch = '\0'
else: ch = data[pos]
template setChar =
## Set ch to current position in string if it's not the last characters
if pos < data.len: ch = data[pos]
template charError =
raise newException(ValueError, CharErrorMsg % [$ch, $pos])
template eat(charToEat: char): bool =
## Skips all whitespace characters and checks if
## current character is *charToEat*. If so, gets the next char
## and returns true
pos += skipWhitespace(data, pos)
setChar()
if ch == charToEat:
nextChar()
true
else: false
# We forward-declare these two procs because we have a recursive dependency
proc parseExpression: Node
proc parseFactor: Node
proc getArgs(): seq[Node] =
result = @[]
if eat('('):
# While there are arguments left
while ch != ')':
result.add parseExpression()
# Skip ',' if we have it. With this we allow things like
# max(1 2 3 4) or atan2(3 5)
if ch == ',': nextChar()
if not eat(')'):
charError()
else:
charError()
proc parseFactor: Node =
# Unary + and -
if eat('+'):
let num = parseFactor()
if num.kind == Num: return Node(kind: Num, val: num.val)
else: return Node(kind: UnaryAdd, unaryNode: num)
elif eat('-'):
let num = parseFactor()
if num.kind == Num: return Node(kind: Num, val: -num.val)
else: return Node(kind: UnaryAdd, unaryNode: num)
let startPos = pos
if eat('('):
result = parseExpression()
echo result
if not eat(')'):
charError()
elif ch in IdentStartChars:
var name = ""
pos += parseIdent(data, name, pos)
setChar()
if eat('('):
pos -= 1
ch = data[pos]
result = Node(kind: Fun, funName: name, args: getArgs())
else:
result = Node(kind: Var, varName: name)
# Numbers ('.' is for numbers like '.5')
elif ch in {'0'..'9', '.'}:
result = Node(kind: Num)
let skipped = parseFloat(data, result.val, pos)
if skipped == 0: charError()
pos += skipped
setChar()
else:
charError()
proc parseTerm: Node =
result = parseFactor()
while true:
var newNode = Node()
if eat('*'): newNode.kind = Mul
elif eat('/'): newNode.kind = Div
elif eat('%'): newNode.kind = Mod
elif eat('^'): newNode.kind = Pow
else: return
newNode.left = result
newNode.right = parseFactor()
if newNode.left.kind == Num and newNode.right.kind == Num:
if newNode.kind == Mul:
result = Node(kind: Num, val: newNode.left.val * newNode.right.val)
elif newNode.kind == Div:
result = Node(kind: Num, val: newNode.left.val / newNode.right.val)
elif newNode.kind == Mod:
result = Node(kind: Num, val: newNode.left.val.mod(newNode.right.val))
else:
result = Node(kind: Num, val: pow(newNode.left.val, newNode.right.val))
else:
result = newNode
proc parseExpression: Node =
result = parseTerm()
while true:
var newNode = Node()
if eat('+'): newNode.kind = Add
elif eat('-'): newNode.kind = Sub
else: return
newNode.left = result
newNode.right = parseTerm()
# Simplifying
if newNode.left.kind == Num and newNode.right.kind == Num:
if newNode.kind == Add:
result = Node(kind: Num, val: newNode.left.val + newNode.right.val)
else:
result = Node(kind: Num, val: newNode.left.val - newNode.right.val)
else:
result = newNode
result = parseExpression()
# If we didn't process all characters in the string
if pos < data.len:
charError()
let a = parse("sin(4*pi) + 5 - 3 ^ 5 + 4%3 * 5 + 35 ^ -1")
echo a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment