# eval_arith.py
#
# Copyright 2009, 2011 Paul McGuire
#
# Expansion on the pyparsing example simpleArith.py, to include evaluation
# of the parsed tokens.
#
# Added support for exponentiation, using right-to-left evaluation of
# operands
#
from pyparsing import (
    Word,
    nums,
    alphas,
    Combine,
    one_of,
    OpAssoc,
    infix_notation,
    Literal,
    ParserElement,
)

ParserElement.enablePackrat()


class EvalConstant:
    "Class to evaluate a parsed constant or variable"
    vars_ = {}

    def __init__(self, tokens):
        self.value = tokens[0]

    def eval(self):
        if self.value in EvalConstant.vars_:
            return EvalConstant.vars_[self.value]
        else:
            return float(self.value)


class EvalSignOp:
    "Class to evaluate expressions with a leading + or - sign"

    def __init__(self, tokens):
        self.sign, self.value = tokens[0]

    def eval(self):
        mult = {"+": 1, "-": -1}[self.sign]
        return mult * self.value.eval()


def operatorOperands(tokenlist):
    "generator to extract operators and operands in pairs"
    it = iter(tokenlist)
    while 1:
        try:
            yield (next(it), next(it))
        except StopIteration:
            break


class EvalPowerOp:
    "Class to evaluate multiplication and division expressions"

    def __init__(self, tokens):
        self.value = tokens[0]

    def eval(self):
        res = self.value[-1].eval()
        for val in self.value[-3::-2]:
            res = val.eval() ** res
        return res


class EvalMultOp:
    "Class to evaluate multiplication and division expressions"

    def __init__(self, tokens):
        self.value = tokens[0]

    def eval(self):
        prod = self.value[0].eval()
        for op, val in operatorOperands(self.value[1:]):
            if op == "*":
                prod *= val.eval()
            if op == "/":
                prod /= val.eval()
        return prod


class EvalAddOp:
    "Class to evaluate addition and subtraction expressions"

    def __init__(self, tokens):
        self.value = tokens[0]

    def eval(self):
        sum = self.value[0].eval()
        for op, val in operatorOperands(self.value[1:]):
            if op == "+":
                sum += val.eval()
            if op == "-":
                sum -= val.eval()
        return sum


class EvalComparisonOp:
    "Class to evaluate comparison expressions"
    opMap = {
        "<": lambda a, b: a < b,
        "<=": lambda a, b: a <= b,
        ">": lambda a, b: a > b,
        ">=": lambda a, b: a >= b,
        "!=": lambda a, b: a != b,
        "=": lambda a, b: a == b,
        "LT": lambda a, b: a < b,
        "LE": lambda a, b: a <= b,
        "GT": lambda a, b: a > b,
        "GE": lambda a, b: a >= b,
        "NE": lambda a, b: a != b,
        "EQ": lambda a, b: a == b,
        "<>": lambda a, b: a != b,
    }

    def __init__(self, tokens):
        self.value = tokens[0]

    def eval(self):
        val1 = self.value[0].eval()
        for op, val in operatorOperands(self.value[1:]):
            fn = EvalComparisonOp.opMap[op]
            val2 = val.eval()
            if not fn(val1, val2):
                break
            val1 = val2
        else:
            return True
        return False


# define the parser
integer = Word(nums)
real = Combine(Word(nums) + "." + Word(nums))
variable = Word(alphas, exact=1)
operand = real | integer | variable

signop = one_of("+ -")
multop = one_of("* /")
plusop = one_of("+ -")
expop = Literal("**")

# use parse actions to attach EvalXXX constructors to sub-expressions
operand.setParseAction(EvalConstant)
arith_expr = infix_notation(
    operand,
    [
        (signop, 1, OpAssoc.RIGHT, EvalSignOp),
        (expop, 2, OpAssoc.LEFT, EvalPowerOp),
        (multop, 2, OpAssoc.LEFT, EvalMultOp),
        (plusop, 2, OpAssoc.LEFT, EvalAddOp),
    ],
)

comparisonop = one_of("< <= > >= != = <> LT GT LE GE EQ NE")
comp_expr = infix_notation(
    arith_expr,
    [
        (comparisonop, 2, OpAssoc.LEFT, EvalComparisonOp),
    ],
)


# sample expressions posted on comp.lang.python, asking for advice
# in safely evaluating them
rules = [
    "( A - B ) = 0",
    "( B - C + B ) = 0",
    "(A + B + C + D + E + F + G + H + I) = J",
    "(A + B + C + D + E + F + G + H) = I",
    "(A + B + C + D + E + F) = G",
    "(A + B + C + D + E) = (F + G + H + I + J)",
    "(A + B + C + D + E) = (F + G + H + I)",
    "(A + B + C + D + E) = F",
    "(A + B + C + D) = (E + F + G + H)",
    "(A + B + C) = D",
    "(A + B + C) = (D + E + F)",
    "(A + B) = (C + D + E + F)",
    "(A + B) = (C + D)",
    "(A + B) = (C - D + E - F - G + H + I + J)",
    "(A + B) = C",
    "(A + B) = 0",
    "(A+B+C+D+E) = (F+G+H+I+J)",
    "(A+B+C+D) = (E+F+G+H)",
    "(A+B+C+D)=(E+F+G+H)",
    "(A+B+C)=(D+E+F)",
    "(A+B)=(C+D)",
    "(A+B)=C",
    "(A-B)=C",
    "(A/(B+C))",
    "(B/(C+D))",
    "(G + H) = I",
    "-0.99 LE ((A+B+C)-(D+E+F+G)) LE 0.99",
    "-0.99 LE (A-(B+C)) LE 0.99",
    "-1000.00 LE A LE 0.00",
    "-5000.00 LE A LE 0.00",
    "A < B",
    "A < 7000",
    "A = -(B)",
    "A = C",
    "A = 0",
    "A GT 0",
    "A GT 0.00",
    "A GT 7.00",
    "A LE B",
    "A LT -1000.00",
    "A LT -5000",
    "A LT 0",
    "G=(B+C+D)",
    "A=B",
    "I = (G + H)",
    "0.00 LE A LE 4.00",
    "4.00 LT A LE 7.00",
    "0.00 LE A LE 4.00 LE E > D",
    "2**2**(A+3)",
]
vars_ = {
    "A": 0,
    "B": 1.1,
    "C": 2.2,
    "D": 3.3,
    "E": 4.4,
    "F": 5.5,
    "G": 6.6,
    "H": 7.7,
    "I": 8.8,
    "J": 9.9,
}

# define tests from given rules
tests = []
for t in rules:
    t_orig = t
    t = t.replace("=", "==")
    t = t.replace("EQ", "==")
    t = t.replace("LE", "<=")
    t = t.replace("GT", ">")
    t = t.replace("LT", "<")
    t = t.replace("GE", ">=")
    t = t.replace("LE", "<=")
    t = t.replace("NE", "!=")
    t = t.replace("<>", "!=")
    tests.append((t_orig, eval(t, vars_)))

# copy vars_ to EvalConstant lookup dict
EvalConstant.vars_ = vars_
failed = 0
for test, expected in tests:
    ret = comp_expr.parseString(test)[0]
    parsedvalue = ret.eval()
    print(test, expected, parsedvalue)
    if abs(parsedvalue - expected) > 1e-6:
        print("<<< FAIL")
        failed += 1
    else:
        print("")

print("")
if failed:
    raise Exception("could not parse")
