from __future__ import division
import math
import operator
from typing import Optional
from pyparsing import (
CaselessLiteral,
Combine,
Forward,
Group,
Literal,
Optional,
Word,
ZeroOrMore,
alphas,
nums,
oneOf,
)
from ..interface import Block
from ..interpreter import Context
class NumericStringParser(object):
"""
Most of this code comes from the fourFn.py pyparsing example
"""
def pushFirst(self, strg, loc, toks):
self.exprStack.append(toks[0])
def pushUMinus(self, strg, loc, toks):
if toks and toks[0] == "-":
self.exprStack.append("unary -")
def __init__(self):
"""
expop :: '^'
multop :: '*' | '/'
addop :: '+' | '-'
integer :: ['+' | '-'] '0'..'9'+
atom :: PI | E | real | fn '(' expr ')' | '(' expr ')'
factor :: atom [ expop factor ]*
term :: factor [ multop factor ]*
expr :: term [ addop term ]*
"""
point = Literal(".")
e = CaselessLiteral("E")
fnumber = Combine(
Word("+-" + nums, nums)
+ Optional(point + Optional(Word(nums)))
+ Optional(e + Word("+-" + nums, nums))
)
ident = Word(alphas, alphas + nums + "_$")
mod = Literal("%")
plus = Literal("+")
minus = Literal("-")
mult = Literal("*")
iadd = Literal("+=")
imult = Literal("*=")
idiv = Literal("/=")
isub = Literal("-=")
div = Literal("/")
lpar = Literal("(").suppress()
rpar = Literal(")").suppress()
addop = plus | minus
multop = mult | div | mod
iop = iadd | isub | imult | idiv
expop = Literal("^")
pi = CaselessLiteral("PI")
expr = Forward()
atom = (
(
Optional(oneOf("- +"))
+ (ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst)
)
| Optional(oneOf("- +")) + Group(lpar + expr + rpar)
).setParseAction(self.pushUMinus)
# by defining exponentiation as "atom [ ^ factor ]..." instead of
# "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right
# that is, 2^3^2 = 2^(3^2), not (2^3)^2.
factor = Forward()
factor << atom + ZeroOrMore((expop + factor).setParseAction(self.pushFirst))
term = factor + ZeroOrMore((multop + factor).setParseAction(self.pushFirst))
expr << term + ZeroOrMore((addop + term).setParseAction(self.pushFirst))
final = expr + ZeroOrMore((iop + expr).setParseAction(self.pushFirst))
# addop_term = ( addop + term ).setParseAction( self.pushFirst )
# general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
# expr << general_term
self.bnf = final
# map operator symbols to corresponding arithmetic operations
epsilon = 1e-12
self.opn = {
"+": operator.add,
"-": operator.sub,
"+=": operator.iadd,
"-=": operator.isub,
"*": operator.mul,
"*=": operator.imul,
"/": operator.truediv,
"/=": operator.itruediv,
"^": operator.pow,
"%": operator.mod,
}
self.fn = {
"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"sinh": math.sinh,
"cosh": math.cosh,
"tanh": math.tanh,
"exp": math.exp,
"abs": abs,
"trunc": lambda a: int(a),
"round": round,
"sgn": lambda a: abs(a) > epsilon and ((a > 0) - (a < 0)) or 0,
"log": lambda a: math.log(a, 10),
"ln": math.log,
"log2": math.log2,
"sqrt": math.sqrt,
}
def evaluateStack(self, s):
op = s.pop()
if op == "unary -":
return -self.evaluateStack(s)
if op in self.opn:
op2 = self.evaluateStack(s)
op1 = self.evaluateStack(s)
return self.opn[op](op1, op2)
elif op == "PI":
return math.pi # 3.1415926535
elif op == "E":
return math.e # 2.718281828
elif op in self.fn:
return self.fn[op](self.evaluateStack(s))
elif op[0].isalpha():
return 0
else:
return float(op)
def eval(self, num_string, parseAll=True):
self.exprStack = []
results = self.bnf.parseString(num_string, parseAll)
return self.evaluateStack(self.exprStack[:])
NSP = NumericStringParser()
[docs]class MathBlock(Block):
ACCEPTED_NAMES = ("math", "m", "+", "calc")
def process(self, ctx: Context):
try:
return str(NSP.eval(ctx.verb.payload.strip(" ")))
except:
return None