Search code examples
pythonexpressionsymbolsevaluate

Evaluate an (almost algebraic) expression without the '*' symbol in python


I have the following content in the value.txt:

2A-25X-8A+34X-5B+11B

If I use MetaFont via terminal bash how below:

#mf
This is METAFONT, Version 2.7182818 (TeX Live 2019/Arch Linux) (preloaded base=mf)
**expr
(/usr/share/texmf-dist/fonts/source/public/knuth-lib/expr.mf
gimme an expr: 2A-25X-8A+34X-5B+11B
>> 6B+9X-6A
gimme an expr:

I can evaluate the expression without the '*' symbol between letters and numbers.

What I want is to do this using Python as cleanly and economically as possible but still without using '*'. I haven't found anything about it yet. I also hope it is a syntax that can be implemented with with open, print = and r.

EDIT

A possible idea would be like this:

with open ("value.txt", "r") as value:
    data = value.read()

#some python method for evaluate value.txt expression and save in variable value2

print = (value2)

Solution

  • Always interested in questions regarding parsing arithmetic. Here is a pyparsing-based solution (albeit a bit longer than you were hoping, and using more than just with, open, etc.).

    The first 30 lines define a class for tallying up the variables, with support for adding, subtracting, and multiplying by an integer. (Integers are modeled as a Tally with a variable of ''.)

    The next 30 lines define the actual parser, and the parse-time actions to convert the parsed tokens into cumulative Tally objects.

    The final 25 lines are tests, including your sample expression.

    The real "smarts" of the parser are in the infixNotation method, which implements the parsing of the various operators, including handling of operator precedence and grouping with ()'s. The use of "3A" to indicate "3 times A" is done by passing None as the multiplication operator. This also supports constructs like "2(A+2B)" to give "2A+4B".

    import pyparsing as pp
    
    # special form of dict to support addition, subtraction, and multiplication, plus a nice repr
    class Tally(dict):
        def __add__(self, other):
            ret = Tally(**self)
            for k, v in other.items():
                ret[k] = ret.get(k, 0) + v
                if k and ret[k] == 0:
                    ret.pop(k)
            return ret
    
        def __mul__(self, other):
            if self[''] == 0:
                return Tally()
            ret = Tally(**other)
            for k in ret:
                ret[k] *= self['']
            return ret
    
        def __sub__(self, other):
            return self + MINUS_1 * other
    
        def __repr__(self):
            ret = ''.join("{}{}{}".format("+" if coeff > 0 else "-", str(abs(coeff)) if abs(coeff) != 1 else "", var)
                              for var, coeff in sorted(self.items()) if coeff)
    
            # leading '+' signs are unnecessary
            ret = ret.lstrip("+")
            return ret
    
    MINUS_1 = Tally(**{'': -1})
    
    var = pp.oneOf(list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
    
    # convert var to a Tally of 1
    var.addParseAction(lambda t: Tally(**{t[0]: 1}))
    integer = pp.pyparsing_common.integer().addParseAction(lambda tokens: Tally(**{'': tokens[0]}))
    
    def add_terms(tokens):
        parsed = tokens[0]
        ret = parsed[0]
        for op, term in zip(parsed[1::2], parsed[2::2]):
            if op == '-':
                ret -= term
            else:
                ret += term
        return ret
    
    def mult_terms(tokens):
        coeff, var = tokens[0]
        return coeff * var
    
    # only the leading minus needs to be handled this way, all others are handled
    # as binary subtraction operators
    def leading_minus(tokens):
        parsed = tokens[0]
        return MINUS_1 * parsed[1]
    leading_minus_sign = pp.StringStart() + "-"
    
    operand = var | integer
    expr = pp.infixNotation(operand,
                            [
                                (leading_minus_sign, 1, pp.opAssoc.RIGHT, leading_minus),
                                (None, 2, pp.opAssoc.LEFT, mult_terms),
                                (pp.oneOf("+ -"), 2, pp.opAssoc.LEFT, add_terms),
                            ])
    
    
    expr.runTests("""\
        B
        B+C
        B+C+3B
        2A
        -2A
        -3Z+42B
        2A+4A-6A
        2A-25X-8A+34X-5B+11B
        3(2A+B)
        -(2A+B)
        -3(2A+B)
        2A+12
        12
        -12
        2A-12
        (5-3)(A+B)
        (3-3)(A+B)
        """)
    

    Gives the output (runTests echoes each test line, followed by the parsed result):

    B
    [B]
    
    B+C
    [B+C]
    
    B+C+3B
    [4B+C]
    
    2A
    [2A]
    
    -2A
    [-2A]
    
    -3Z+42B
    [42B-3Z]
    
    2A+4A-6A
    []
    
    2A-25X-8A+34X-5B+11B
    [-6A+6B+9X]
    
    3(2A+B)
    [6A+3B]
    
    -(2A+B)
    [-2A-B]
    
    -3(2A+B)
    [-6A-3B]
    
    2A+12
    [12+2A]
    
    12
    [12]
    
    -12
    [-12]
    
    2A-12
    [-12+2A]
    
    (5-3)(A+B)
    [2A+2B]
    
    (3-3)(A+B)
    []
    

    To show how to use expr to parse your expression string, see this code:

    result = expr.parseString("2A-25X-8A+34X-5B+11B")
    print(result)
    print(result[0])
    print(type(result[0]))
    
    # convert back to dict
    print({**result[0]})
    

    Prints:

    [-6A+6B+9X]
    -6A+6B+9X
    <class '__main__.Tally'>
    {'B': 6, 'A': -6, 'X': 9}