Skip to content

Instantly share code, notes, and snippets.

@Yardanico
Last active March 15, 2018 19:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Yardanico/9fcdb067c9f52330e1aabb469f77dd60 to your computer and use it in GitHub Desktop.
Save Yardanico/9fcdb067c9f52330e1aabb469f77dd60 to your computer and use it in GitHub Desktop.
import math, strutils
const
ArgsErrorMsg = "Incorrect number of arguments at pos $1 in function `$2`"
CharErrorMsg = "Unexpected char `$1` at pos $2"
proc eval(data: string): float =
## Evaluates math expression from string *data* and returns result as a float
var data = data.toLowerAscii()
var
pos = -1 ## Current position
ch: char ## Current char
template nextChar =
## Gets next char
inc pos
ch = data[pos]
proc eat(charToEat: char): bool {.inline.} =
## Eats all chars until charToEat is found
while ch == ' ': nextChar()
if ch == charToEat:
nextChar()
result = true
# Forward-declare these two procs (since we have a recursive dependency)
proc parseExpression: float
proc parseFactor: float
proc parseArgumentsAux(argsNum = -1): seq[float] =
## Parses any number of arguments (except 1)
if argsNum != -1:
result = newSeqOfCap[float](argsNum)
else:
result = newSeq[float]()
# No arguments at all
if ch != ',': return
while ch == ',':
nextChar()
result.add parseFactor()
nextChar()
if argsNum != -1 and argsNum != result.len:
# Return nothing
result = nil
template getArgs(argsNum = -1, allowZeroArgs = false): untyped {.dirty.} =
var data = @[parseFactor()]
# If we need to parse more than 1 argument
if argsNum != 1: data.add parseArgumentsAux(argsNum)
# If number of given/needed arguments is wrong:
if data.isNil or (not allowZeroArgs and data.len == 0):
raise newException(ValueError, ArgsErrorMsg % [$pos, $funcName])
data
template getArg(): untyped {.dirty.} =
getArgs(1)[0]
proc parseFactor: float =
# Unary + and -
if eat('+'): return parseFactor()
elif eat('-'): return -parseFactor()
let startPos = pos
# Parentheses
if eat('('):
# A hack to allow zero number of arguments by checking for ')'
# XXX: Check if it will break something, remove it if so
if ch != ')': result = parseExpression()
discard eat(')')
# Numbers
elif ch in {'0'..'9', '.'}:
while ch in {'0'..'9', '.'}: nextChar()
result = parseFloat(data[startPos..<pos])
elif ch in {'a'..'z'}:
while ch in {'a'..'z'}: nextChar()
let funcName = data[startPos..<pos]
case funcName
# Functions
of "abs": result = abs(getArg())
of "acos", "arccos": result = arccos(getArg())
of "asin", "arcsin": result = arcsin(getArg())
of "atan", "arctan": result = arctan(getArg())
of "atan2", "arctan2":
let args = getArgs()
result = arctan2(args[0], args[1])
of "ceil": result = ceil(getArg())
of "cos": result = cos(degToRad(getArg()))
of "cosh": result = cosh(getArg())
of "e": result = math.E
of "exp": result = exp(getArg())
of "sqrt": result = sqrt(getArg())
of "fac": result = float fac(int(getArg()))
of "floor": result = floor(getArg())
of "ln": result = ln(getArg())
of "log": result = log10(getArg())
of "max": result = max(getArgs())
of "min": result = min(getArgs())
of "binom", "ncr":
let args = getArgs()
result = float binom(int args[0], int args[1])
of "npr":
let args = getArgs()
result = float binom(int args[0], int args[1]) * fac(int args[1])
of "pow":
let args = getArgs()
result = pow(args[0], args[1])
of "sin": result = sin(degToRad(getArg()))
of "sinh": result = sinh(getArg())
of "tan": result = tan(degToRad(getArg()))
of "tanh": result = tanh(getArg())
# Constants
of "pi": result = math.PI
of "tau": result = math.TAU
else:
raise newException(ValueError, "Unknown function: " & funcName)
# It's something else
else:
raise newException(ValueError, CharErrorMsg % [$ch, $pos])
# Exponential
if eat('^'): result = result.pow(parseFactor())
proc parseTerm: float =
result = parseFactor()
while true:
if eat('*'): result *= parseTerm()
elif eat('/'): result /= parseTerm()
else: return
proc parseExpression: float =
result = parseTerm()
while true:
if eat('+'): result += parseTerm()
elif eat('-'): result -= parseTerm()
else: return
nextChar()
result = parseExpression()
if pos < data.len:
raise newException(ValueError, CharErrorMsg % [$ch, $pos])
when isMainModule:
# Set our Ctrl+C hook
proc shutdown() {.noconv.} =
echo("\nGoodbye!")
quit(0)
setControlCHook(shutdown)
while true:
stdout.write("> ")
let
mathExpr = readLine(stdin)
if mathExpr == "exit":
quit(0)
try:
let result = eval(mathExpr)
echo("$1 = $2" % [mathExpr, $result])
except:
echo getCurrentExceptionMsg()
continue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment