Sun, 19 Apr 2015 22:02:02 +0300
Major refactor: moved calculator routines into a Calculator class.
Added attributes to calculator.
calc.py | file | annotate | diff | comparison | revisions | |
mod_util.py | file | annotate | diff | comparison | revisions |
--- a/calc.py Sun Apr 19 20:52:49 2015 +0300 +++ b/calc.py Sun Apr 19 22:02:02 2015 +0300 @@ -206,361 +206,409 @@ Symbols = sorted (Symbols, key=lambda x: len (x), reverse=True) PreferredBase = 10 -def parse_number (expr): - """Tries to parse a number from the expression. Returns (value, length) on success.""" +def set_preferred_base(x): global PreferredBase - i = 0 - value = None - base = 10 - - # Possibly it's hexadecimal - if expr[0:2].lower() == '0x': - base = PreferredBase = 0x10 - digits = string.hexdigits - digitname = 'hexit' - i = 2 - elif expr[0:2].lower() == '0b': - base = PreferredBase = 0b10 - digits = ['0', '1'] - digitname = 'bit' - i = 2 - - if base != 10: - # Skip leading zeroes - while i < len (expr) and expr[i] == '0': - i += 1 - - startingIndex = i - while i < len (expr) and expr[i] in digits: - i += 1 - - if i < len(expr) and expr[i] == '.': - raise ValueError ('non-decimal floating point numbers are not supported') - - if i == startingIndex: - if i < len (expr): - raise ValueError ('not a valid %s "%s" in %s' % (digitname, expr[i], expr[0:i+1])) - else: - raise ValueError ('end of expression encountered while parsing ' - 'base-%d literal' % base) - - return (complex (int (expr[startingIndex:i], base), 0), i) - - if expr[0] == 'i': - # Special case, we just have 'i' -- need special handling here because otherwise this would - # call float('') in the complex number routine, which throws an error. - # TODO: maybe 'i' can be a symbol instead? - return (1j, 1) - - # Try parse increasingly long substrings until we are unable to create a float or complex number - # from it. - try: - while i < len (expr): - if expr[i] == 'i': - value = complex (0.0, float (expr[:i])) - else: - value = complex (float (expr [:i + 1]), 0.0) - i += 1 - - return (value, i) - except ValueError: - if i != 0: - # Got a number (the error happened when we tried to parse past the number) - return (value, i) - else: - # The error happened on the first character. So this is not a number. - return None - -def parse_symbol (expr): - for sym in Symbols: - if expr[:len (sym)] == sym: - return sym - - return None - -def tokenize (expr): - global PreferredBase - PreferredBase = 10 - expr = re.sub ('\s+', '', expr.strip()) - i=0 - tokens = [] - - while i < len(expr): - sym = parse_symbol (expr[i:]) - - if sym: - symtype = SymbolTypes[sym] - if symtype == SymbolType.CONSTANT: - tokens.append (Constants[sym]) - else: - tokens.append (sym) - - i += len(sym) - continue - - result = parse_number (expr[i:]) - if result: - num, length = result - tokens.append (num) - i += length - continue - - raise ValueError ("""bad expression, couldn't parse: %s""" % expr[i:]) - - return tokens + PreferredBase = x def rindex (li, value): return (len(li) - 1) - li[::-1].index(value) -def process_parens (expr): - """Processes parentheses of expr into sublists in-place. - [1.0, '*', '(', 3.5, '+', 1j, ')'] - -> [1.0, '*', [3.5, '+', 1j]]""" - if '(' not in expr and ')' not in expr: - return - - try: - parenStart = rindex (expr, '(') - parenEnd = expr.index (')', parenStart) - except ValueError: - raise ValueError ("""mismatched parentheses in expression: %s""" % expr) - - subexpr = expr[parenStart + 1:parenEnd] - del expr[parenStart + 1:parenEnd + 1] - expr[parenStart] = subexpr - process_parens (expr) - -def process_functions (expr): - """Processes functions in-place""" - i = 0 - while i < len (expr): - if type (expr[i]) is list: - process_functions (expr[i]) - - if (type(expr[i]) is not str) or (expr[i] not in Functions): - i += 1 - continue - - # Ensure that arguments follow - if (i + 1 >= len (expr)) or (type (expr[i + 1]) is not list): - raise ValueError ("""function %s used without arguments""" % expr[i]) - - args = expr[i + 1] - del expr[i + 1] - expr[i] = FunctionCall (expr[i], args) - i += 1 - -def is_operand (x): - # Operands can be either lists (which also mean parens, thus can be single-expressions) - # or complex numbers - return type(x) in [complex, list] - -def find_fitting_operator (sym, numOperands): - # Pass 1: exact numOperands match - for op in Operators: - if op.symbol != sym: - continue - - if op.operands == numOperands: - # print '%s, %d -> %s (1st pass)' % (sym, numOperands, op) - return op - - # Pass 2: by symbol - for op in Operators: - if op.symbol == sym: - # print '%s, %d -> %s (2nd pass)' % (sym, numOperands, op) - return op - - raise ValueError ('''unknown operator %s!''' % sym) - -def process_operators (expr): - """Processes operators""" - i = 0 - - # Find all operators in this expression - while i < len (expr): - if type (expr[i]) is list: - process_functions (expr[i]) - process_operators (expr[i]) - - if type (expr[i]) is FunctionCall: - process_functions (expr[i].args) - process_operators (expr[i].args) - - if (type(expr[i]) is not str) or (expr[i] not in OperatorSymbols): - i += 1 - continue - - args = [] - argIndices = [] - if i > 0 and is_operand (expr[i - 1]): - args.append (expr[i - 1]) - argIndices.append (i - 1) - - if i - 1 < len(expr) and is_operand (expr[i + 1]): - args.append (expr[i + 1]) - argIndices.append (i + 1) - - # Resolve operators with the same symbol based on operand count - numOperands = 0 - for arg in args: - if is_operand (arg): - numOperands += 1 - - expr[i] = find_fitting_operator (expr[i], numOperands) - i += 1 - -def find_priority_operator (expr): - """Finds the operator with most priority in the expression""" - bestOp = None - bestOpIndex = -1 - - for i in range (0, len (expr)): - sym = expr[i] - - if type (sym) is not Operator: - continue - - if not bestOp or sym.priority < bestOp.priority: - bestOp = sym - bestOpIndex = i - - return (bestOp, bestOpIndex) - def realPrint (x): print x -tabs='' -def evaluate (expr, verbose=False): - global tabs - printFunc = realPrint if verbose else lambda x:None - printFunc (tabs + 'Preprocess: %s' % expr) +Attributes = { + 'hex': lambda self: self.set_preferred_base (0x10), + 'binary': lambda self: self.set_preferred_base (0b10), + 'decimal': lambda self: self.set_preferred_base (10), +} + +Attributes['bin'] = Attributes['binary'] +Attributes['dec'] = Attributes['decimal'] +AttributeNames = sorted ([key for key in Attributes], key=lambda x:len(x), reverse=True) + +class Calculator (object): + def __init__ (self): + self.preferred_base = None + + def set_preferred_base (self, x): + self.preferred_base = x + + def trim_spaces (self, expr): + return re.sub ('\s+', '', expr.strip()) + + def parse_attributes (self, expr): + if expr[0] != '<': + return expr + + i = 1 + while expr[i] != '>': + if expr[i] == '|': + i += 1 + + for name in AttributeNames: + if expr[i:i + len(name)] == name: + Attributes[name] (self) + i += len(name) - # If there are sub-expressions in here, those need to be evaluated first - i = 0 - while i < len (expr): - sym = expr[i] + if expr[i] == '>': + break + + if expr[i] != '|': + raise ValueError ('malformed attributes') + break + else: + raise ValueError ('bad attributes: %s' % expr[i:]) + + return expr[i + 1:] + + def parse_number (self, expr): + """Tries to parse a number from the expression. Returns (value, length) on success.""" + i = 0 + value = None + base = 10 - if type (sym) is list and sym: - printFunc (tabs + 'Evaluating sub-expression: %s' % (sym)) - tabs += '\t' - expr[i] = evaluate (list (sym), verbose) - tabs = tabs[:-1] - printFunc (tabs + '-> %s' % expr[i]) + # Possibly it's hexadecimal + if expr[0:2].lower() == '0x': + base = 0x10 + digits = string.hexdigits + digitname = 'hexit' + i = 2 + elif expr[0:2].lower() == '0b': + base = 0b10 + digits = ['0', '1'] + digitname = 'bit' + i = 2 + + if base != 10: + if not self.preferred_base: + self.preferred_base = base + + # Skip leading zeroes + while i < len (expr) and expr[i] == '0': + i += 1 - # If there are function calls, evaluate those - if type (sym) is FunctionCall: - tabs += '\t' - if sym.args: - sym.args = [evaluate (sym.args, verbose)] - tabs = tabs[:-1] + startingIndex = i + while i < len (expr) and expr[i] in digits: + i += 1 + + if i < len(expr) and expr[i] == '.': + raise ValueError ('non-decimal floating point numbers are not supported') + + if i == startingIndex: + if i < len (expr): + raise ValueError ('not a valid %s "%s" in %s' % (digitname, expr[i], expr[0:i+1])) + else: + raise ValueError ('end of expression encountered while parsing ' + 'base-%d literal' % base) + + return (complex (int (expr[startingIndex:i], base), 0), i) + + if expr[0] == 'i': + # Special case, we just have 'i' -- need special handling here because otherwise this would + # call float('') in the complex number routine, which throws an error. + # TODO: maybe 'i' can be a symbol instead? + return (1j, 1) + + # Try parse increasingly long substrings until we are unable to create a float or complex number + # from it. + try: + while i < len (expr): + if expr[i] == 'i': + value = complex (0.0, float (expr[:i])) + else: + value = complex (float (expr [:i + 1]), 0.0) + i += 1 - printFunc (tabs + 'Evaluating function call: %s' % (sym)) - expr[i] = Functions[sym.funcname]['function'] (*sym.args) - printFunc (tabs + '-> %s' % expr[i]) - - i += 1 + return (value, i) + except ValueError: + if i != 0: + # Got a number (the error happened when we tried to parse past the number) + return (value, i) + else: + # The error happened on the first character. So this is not a number. + return None + + def parse_symbol (self, expr): + for sym in Symbols: + if expr[:len (sym)] == sym: + return sym + + return None + + def tokenize (self, expr): + i=0 + tokens = [] - printFunc (tabs + 'Evaluate: %s' % expr) - runaway = 0 - while True: - runaway += 1 - if runaway > 1000: - raise ValueError ('infinite loop detected') + while i < len(expr): + sym = self.parse_symbol (expr[i:]) + + if sym: + symtype = SymbolTypes[sym] + if symtype == SymbolType.CONSTANT: + tokens.append (Constants[sym]) + else: + tokens.append (sym) + + i += len(sym) + continue + + result = self.parse_number (expr[i:]) + if result: + num, length = result + tokens.append (num) + i += length + continue + + raise ValueError ("""bad expression, couldn't parse: %s""" % expr[i:]) - op, i = find_priority_operator (expr) - if not op: - break + return tokens + + def process_parens (self, expr): + """Processes parentheses of expr into sublists in-place. + [1.0, '*', '(', 3.5, '+', 1j, ')'] + -> [1.0, '*', [3.5, '+', 1j]]""" + if '(' not in expr and ')' not in expr: + return + + try: + parenStart = rindex (expr, '(') + parenEnd = expr.index (')', parenStart) + except ValueError: + raise ValueError ("""mismatched parentheses in expression: %s""" % expr) - if op.operands == 2: - argIndices = [i - 1, i + 1] - else: - argIndices = [i + 1] + subexpr = expr[parenStart + 1:parenEnd] + del expr[parenStart + 1:parenEnd + 1] + expr[parenStart] = subexpr + self.process_parens (expr) + + def process_functions (self, expr): + """Processes functions in-place""" + i = 0 + while i < len (expr): + if type (expr[i]) is list: + self.process_functions (expr[i]) + + if (type(expr[i]) is not str) or (expr[i] not in Functions): + i += 1 + continue + + # Ensure that arguments follow + if (i + 1 >= len (expr)) or (type (expr[i + 1]) is not list): + raise ValueError ("""function %s used without arguments""" % expr[i]) - args = [expr[x] for x in argIndices] - argIndices = sorted (argIndices, reverse=True) - printFunc (tabs + 'Processing: (%s, %d) with args %s' % (op, i, args)) - expr[i] = op.function (*args) - printFunc (tabs + '-> %s' % expr[i]) + args = expr[i + 1] + del expr[i + 1] + expr[i] = FunctionCall (expr[i], args) + i += 1 + + def is_operand (self, x): + # Operands can be either lists (which also mean parens, thus can be single-expressions) + # or complex numbers + return type(x) in [complex, list] + + def find_fitting_operator (self, sym, numOperands): + # Pass 1: exact numOperands match + for op in Operators: + if op.symbol != sym: + continue - for i2 in argIndices: - del expr[i2] + if op.operands == numOperands: + return op + + # Pass 2: by symbol + for op in Operators: + if op.symbol == sym: + return op + + raise ValueError ('''unknown operator %s!''' % sym) + + def process_operators (self, expr): + """Processes operators""" + i = 0 + + # Find all operators in this expression + while i < len (expr): + if type (expr[i]) is list: + self.process_functions (expr[i]) + self.process_operators (expr[i]) - printFunc (tabs + 'Result: %s' % expr[0]) + if type (expr[i]) is FunctionCall: + self.process_functions (expr[i].args) + self.process_operators (expr[i].args) + + if (type(expr[i]) is not str) or (expr[i] not in OperatorSymbols): + i += 1 + continue + + args = [] + argIndices = [] + if i > 0 and self.is_operand (expr[i - 1]): + args.append (expr[i - 1]) + argIndices.append (i - 1) + + if i - 1 < len(expr) and self.is_operand (expr[i + 1]): + args.append (expr[i + 1]) + argIndices.append (i + 1) - if len(expr) != 1: - printFunc (tabs + 'Bad expression detected, tokens: %s' % expr) - raise ValueError ('malformed expression') + # Resolve operators with the same symbol based on operand count + numOperands = 0 + for arg in args: + if self.is_operand (arg): + numOperands += 1 + + expr[i] = self.find_fitting_operator (expr[i], numOperands) + i += 1 - return expr[0] + def find_priority_operator (self, expr): + """Finds the operator with most priority in the expression""" + bestOp = None + bestOpIndex = -1 + + for i in range (0, len (expr)): + sym = expr[i] + + if type (sym) is not Operator: + continue -def repr_number (x): - """Returns a string representation for a real number""" - if math.fabs (x - math.floor(x)) < epsilon and PreferredBase != 10: - digits='0123456789abcdef' - assert PreferredBase <= len (digits), '''preferred base %d is too large''' % PreferredBase + if not bestOp or sym.priority < bestOp.priority: + bestOp = sym + bestOpIndex = i + + return (bestOp, bestOpIndex) + + def evaluate (self, expr, verbose=False): + printFunc = realPrint if verbose else lambda x:None + printFunc (self.tabs + 'Preprocess: %s' % expr) + + # If there are sub-expressions in here, those need to be evaluated first + i = 0 + while i < len (expr): + sym = expr[i] - divisor = PreferredBase - rep = '' - x = int (x) + if type (sym) is list and sym: + printFunc (self.tabs + 'Evaluating sub-expression: %s' % (sym)) + self.tabs += '\t' + expr[i] = self.evaluate (list (sym), verbose) + self.tabs = self.tabs[:-1] + printFunc (self.tabs + '-> %s' % expr[i]) + + # If there are function calls, evaluate those + if type (sym) is FunctionCall: + self.tabs += '\t' + if sym.args: + sym.args = [self.evaluate (sym.args, verbose)] + self.tabs = self.tabs[:-1] + + printFunc (self.tabs + 'Evaluating function call: %s' % (sym)) + expr[i] = Functions[sym.funcname]['function'] (*sym.args) + printFunc (self.tabs + '-> %s' % expr[i]) + + i += 1 + + printFunc (self.tabs + 'Evaluate: %s' % expr) runaway = 0 - - while x > 0: + while True: runaway += 1 if runaway > 1000: - raise ValueError('runaway triggered') + raise ValueError ('infinite loop detected') + + op, i = self.find_priority_operator (expr) + if not op: + break + + if op.operands == 2: + argIndices = [i - 1, i + 1] + else: + argIndices = [i + 1] - i = (x % divisor) / (divisor / PreferredBase) - x -= i * (divisor / PreferredBase) - rep += digits[i] - divisor *= PreferredBase + args = [expr[x] for x in argIndices] + argIndices = sorted (argIndices, reverse=True) + printFunc (self.tabs + 'Processing: (%s, %d) with args %s' % (op, i, args)) + expr[i] = op.function (*args) + printFunc (self.tabs + '-> %s' % expr[i]) - rep += 'x' if PreferredBase == 16 else 'b' - rep += '0' - return rep[::-1] + for i2 in argIndices: + del expr[i2] - rep = '%.10f' % x + printFunc (self.tabs + 'Result: %s' % expr[0]) - if '.' in rep: - while rep[-1] == '0': - rep = rep[:-1] + if len(expr) != 1: + printFunc (self.tabs + 'Bad expression detected, tokens: %s' % expr) + raise ValueError ('malformed expression') + + return expr[0] - if rep[-1] == '.': - rep = rep[:-1] + def repr_number (self, x): + """Returns a string representation for a real number""" + base = self.preferred_base if self.preferred_base else 10 + if math.fabs (x - math.floor(x)) < epsilon and base != 10: + digits='0123456789abcdef' + assert base <= len (digits), '''base %d is too large''' % base - + divisor = base + rep = '' + x = int (x) + runaway = 0 - return rep + while x > 0: + runaway += 1 + if runaway > 1000: + raise ValueError('runaway triggered') -def repr_imaginary (x): - rep = repr_number (x) + i = (x % divisor) / (divisor / base) + x -= i * (divisor / base) + rep += digits[i] + divisor *= base - if rep == '1': - return 'i' + rep += 'x' if base == 16 else 'b' + rep += '0' + return rep[::-1] + + rep = '%.10f' % x + + if '.' in rep: + while rep[-1] == '0': + rep = rep[:-1] + + if rep[-1] == '.': + rep = rep[:-1] - if rep == '-1': - return '-i' + return rep + + def repr_imaginary (self, x): + rep = self.repr_number (x) - return rep + 'i' + if rep == '1': + return 'i' + + if rep == '-1': + return '-i' + + return rep + 'i' -def represent (x): - """Returns a string representation of a float or complex number""" - if math.fabs (x.imag) > epsilon: - if math.fabs (x.real) > epsilon: - # Complex number - return '%s %s %s' % (repr_number (x.real), - '+' if x.imag >= 0 else '-', - repr_imaginary (math.fabs (x.imag))) + def represent (self, x): + """Returns a string representation of a float or complex number""" + if math.fabs (x.imag) > epsilon: + if math.fabs (x.real) > epsilon: + # Complex number + return '%s %s %s' % (self.repr_number (x.real), + '+' if x.imag >= 0 else '-', + self.repr_imaginary (math.fabs (x.imag))) + else: + # Pure imaginary number + return self.repr_imaginary (x.imag) else: - # Pure imaginary number - return repr_imaginary (x.imag) - else: - # Real number - return repr_number (x.real) + # Real number + return self.repr_number (x.real) -def calc (expr, verbose=False): - expr = tokenize (expr) - process_parens (expr) - process_functions (expr) - process_operators (expr) - return represent (evaluate (expr, verbose)) + def calc (self, expr, verbose=False): + self.state = {} + self.tabs = '' + expr = self.trim_spaces (expr) + expr = self.parse_attributes (expr) + expr = self.tokenize (expr) + self.process_parens (expr) + self.process_functions (expr) + self.process_operators (expr) + result = self.evaluate (expr, verbose) + return self.represent (result)
--- a/mod_util.py Sun Apr 19 20:52:49 2015 +0300 +++ b/mod_util.py Sun Apr 19 22:02:02 2015 +0300 @@ -201,7 +201,7 @@ ', '.join (sorted ([name for name, data in calc.Functions.iteritems()]))) def cmd_calc (bot, reply, args, **rest): - reply (calc.calc (args['expression'])) + reply (calc.Calculator().calc (args['expression'])) # expr = args['expression'] # try: # Substitute some mathematical constants