Welcome, guest | Sign In | My Account | Store | Cart

This is a complete rewrite of recipe 576790. While aiming to maintain similar functionality and continuing its implementation for self-academic purposes, a much cleaner parser / tokenizer and operator execution engine were developed. A slightly different math syntax is supported in this version, but it is arguably better and more capable than it previously was. Base prefixes are a feature now supported, and the single downgrade is calculating with integers instead of floats.

Python, 217 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#! /usr/bin/env python
import operator

################################################################################

def evaluate(source, local):
    "Execute all math operations found in the source."
    for expression in expressions(source):
        local['_'] = tokens(expression).evaluate(local)

def expressions(source):
    "Separate expressions and yield each individually."
    lines = source.replace('\r\n', '\n').replace('\r', '\n').split('\n')
    uncommented = map(lambda line: line.split('#', 1)[0], lines)
    for line in uncommented:
        if line and not line.isspace():
            for expression in line.split(';'):
                yield expression

def tokens(string):
    "Build an expression tree by tokenizing expression."
    evaluator = _tokens(string)
    if isinstance(evaluator, Operation) and \
       evaluator._Operation__symbol == Operation.ASSIGNMENT:
        return evaluator
    return Print(evaluator)

def _tokens(string):
    "Private module function: recursively builds a tree."
    expression = string.strip()
    if not expression:
        raise SyntaxError('empty expression')
    divisions = Operation.split(expression)
    if divisions:
        left, symbol, right = divisions
        return Operation(_tokens(left), symbol, _tokens(right))
    if len(expression.split()) > 1:
        raise SyntaxError(expression)
    if expression.startswith('0x'):
        return Constant(int(expression[2:], 16))
    if expression.startswith('0d'):
        return Constant(int(expression[2:], 10))
    if expression.startswith('0o'):
        return Constant(int(expression[2:], 8))
    if expression.startswith('0q'):
        return Constant(int(expression[2:], 4))
    if expression.startswith('0b'):
        return Constant(int(expression[2:], 2))
    if expression.isdigit():
        return Constant(int(expression))
    if expression.isidentifier():
        return Variable(expression)
    raise SyntaxError(expression)

################################################################################

class Expression:

    "Abstract class for Expression objects."

    def __init__(self):
        "Initialize the Expression object."
        raise NotImplementedError()

    def evaluate(self, bindings):
        "Calculate the value of this object."
        raise NotImplementedError()

    def __repr__(self):
        "Return a representation of this object."
        klass = self.__class__.__name__
        private = '_{}__'.format(klass)
        args = []
        for name in vars(self):
            if name.startswith(private):
                key = name[len(private):]
                value = getattr(self, name)
                args.append('{}={!r}'.format(key, value))
        return '{}({})'.format(klass, ', '.join(args))

################################################################################

class Constant(Expression):

    "Class for storing all math constants."

    def __init__(self, value):
        "Initialize the Constant object."
        self.__value = value

    def evaluate(self, bindings):
        "Calculate the value of this object."
        return self.__value

################################################################################

class Variable(Expression):

    "Class for storing all math variables."

    def __init__(self, name):
        "Initialize the Variable object."
        self.__name = name

    def evaluate(self, bindings):
        "Calculate the value of this object."
        if self.__name not in bindings:
            raise NameError(self.__name)
        return bindings[self.__name]

################################################################################

class Operation(Expression):

    "Class for executing math operations."

    ASSIGNMENT = '->'
    OPERATORS = {ASSIGNMENT: lambda a, b: None,
                 'and': lambda a, b: a and b,
                 'or': lambda a, b: a or b,
                 '+': operator.add,
                 '-': operator.sub,
                 '*': operator.mul,
                 '/': operator.floordiv,
                 '%': operator.mod,
                 '**': operator.pow,
                 '&': operator.and_,
                 '|': operator.or_,
                 '^': operator.xor,
                 '>>': operator.rshift,
                 '<<': operator.lshift,
                 '==': operator.eq,
                 '!=': operator.ne,
                 '>': operator.gt,
                 '>=': operator.ge,
                 '<': operator.lt,
                 '<=': operator.le}

    def __init__(self, left, symbol, right):
        "Initialize the Operation object."
        self.__left = left
        self.__symbol = symbol
        self.__right = right

    def evaluate(self, bindings):
        "Calculate the value of this object."
        if self.__symbol == self.ASSIGNMENT:
            if not isinstance(self.__right, Variable):
                raise TypeError(self.__right)
            key = self.__right._Variable__name
            value = self.__left.evaluate(bindings)
            bindings[key] = value
            return value
        return self.__operate(bindings)

    def __operate(self, bindings):
        "Execute operation defined by symbol."
        if self.__symbol not in self.OPERATORS:
            raise SyntaxError(self.__symbol)
        a = self.__left.evaluate(bindings)
        b = self.__right.evaluate(bindings)
        return self.OPERATORS[self.__symbol](a, b)

    __operators = sorted(OPERATORS, key=len, reverse=True)

    @classmethod
    def split(cls, expression):
        "Split expression on rightmost symbol."
        tail = cls.__split(expression)
        if tail:
            symbol, right = tail
            return expression[:-sum(map(len, tail))], symbol, right

    @classmethod
    def __split(cls, expression):
        "Private class method: help with split."
        for symbol in cls.__operators:
            if symbol in expression:
                right = expression.rsplit(symbol, 1)[1]
                tail = cls.__split(right)
                if tail is None:
                    return symbol, right
                return tail

################################################################################

class Print(Expression):

    "Class for printing all math results."

    def __init__(self, expression):
        "Initialize the Print object."
        self.__expression = expression

    def evaluate(self, bindings):
        "Calculate the value of this object."
        value = self.__expression.evaluate(bindings)
        print(value)
        return value

################################################################################

def test():
    "Run a simple demo that shows evaluator's capability."
    from sys import exc_info, stderr
    from traceback import format_exception_only
    local = {}
    while True:
        try:
            evaluate(input('>>> '), local)
        except EOFError:
            break
        except:
            stderr.write(format_exception_only(*exc_info()[:2])[-1])

if __name__ == '__main__':
    test()

2 comments

Stephen Chappell (author) 13 years, 4 months ago  # | flag

This is a small sample exchange with the program:

>>> 1->a ; 2->b ; 3->c # create a few variables
>>> a;b;c # show the value of those variables
1
2
3
>>> a + b * c # evaluate from left to right
9
>>> _ * _ # access the last calculated value
81
>>> 3 ** 4 == _ # run comparisons on values
True
>>> _ + 1 == 2 ; 1 != 1 + 1 # use booleans as well
True
1
>>> 1 -> x + 1 -> y + 1 -> z ;a==x;b==y;c==z
True
True
True
>>> # multiple assignments are also supported
>>> be careful, or you could get a syntax error
SyntaxError: be careful,
>>>
Sunjay Varma 13 years, 4 months ago  # | flag

This is really cool! Thanks!