calc.py

changeset 123
aeb0d0788869
child 124
7b2cd8b1ba86
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calc.py	Sun Apr 19 19:45:42 2015 +0300
@@ -0,0 +1,474 @@
+import re
+import cmath
+import math
+import random
+import time
+import operator
+from copy import deepcopy
+
+epsilon = 1e-10
+
+# http://stackoverflow.com/a/2182437
+class Enum (set):
+	def __init__ (self, *args):
+		super (Enum, self).__init__ (args)
+
+	def __getattr__ (self, name):
+		if name in self:
+			return name
+		raise AttributeError
+
+	def __setattr__ (self, name, value):
+		raise AttributeError
+
+class Operator (object):
+	def __init__ (self, name, symbol, operands, priority, function):
+		self.name = name
+		self.symbol = symbol
+		self.operands = operands
+		self.priority = priority
+		self.function = function
+
+	def __str__ (self):
+		return '''<operator %s>''' % self.name
+
+	def __repr__ (self):
+		return self.__str__()
+
+class FunctionCall (object):
+	def __init__ (self, funcname, args):
+		assert (type(args) is list)
+		self.funcname = funcname
+		self.args = args
+
+	def __str__ (self):
+		return '''<function %s (%s)>''' % (self.funcname, self.args)
+
+	def __repr__ (self):
+		return self.__str__()
+
+def do_realf (func, *args):
+	for x in args:
+		if x.imag:
+			raise ValueError ('%s called with a complex number' % func.__name__)
+
+	return func (*[x.real for x in args])
+
+def do_intf (func, *args):
+	for x in args:
+		if x.imag:
+			raise ValueError ('%s called with a complex number' % func.__name__)
+
+		if x.real - math.floor (x.real):
+			raise ValueError ('%s called with a floating point number' % func.__name__)
+
+	return func (*[int (x.real) for x in args])
+
+def realf (func):
+	return lambda *args: do_realf (func, *args)
+
+def intf (func):
+	return lambda *args: do_intf (func, *args)
+
+Operators = []
+OperatorData = {
+	'lneg':		{ 'symbol': '!',  'operands': 1, 'priority': 5, 'function': lambda x: not x },
+	'compl':	{ 'symbol': '~',  'operands': 1, 'priority': 5, 'function': intf (operator.inv) },
+	'neg':		{ 'symbol': '-',  'operands': 1, 'priority': 5, 'function': lambda x: -x },
+	'pow':		{ 'symbol': '**', 'operands': 2, 'priority': 10, 'function': lambda x, y: x ** y },
+	'mul':		{ 'symbol': '*',  'operands': 2, 'priority': 50, 'function': lambda x, y: x * y },
+	'div':		{ 'symbol': '/',  'operands': 2, 'priority': 50, 'function': lambda x, y: x / y },
+	'mod':		{ 'symbol': '%',  'operands': 2, 'priority': 50, 'function': lambda x, y: math.fmod (x, y) },
+	'add':		{ 'symbol': '+',  'operands': 2, 'priority': 100, 'function': lambda x, y: x + y },
+	'sub':		{ 'symbol': '-',  'operands': 2, 'priority': 100, 'function': lambda x, y: x - y },
+	'eq':		{ 'symbol': '==', 'operands': 2, 'priority': 500, 'function': lambda x, y: x == y },
+	'neq':		{ 'symbol': '!=', 'operands': 2, 'priority': 500, 'function': lambda x, y: x != y },
+	'lt':		{ 'symbol': '<',  'operands': 2, 'priority': 500, 'function': lambda x, y: x < y },
+	'lteq':		{ 'symbol': '<=', 'operands': 2, 'priority': 500, 'function': lambda x, y: x <= y },
+	'gt':		{ 'symbol': '>',  'operands': 2, 'priority': 500, 'function': lambda x, y: x > y },
+	'gteq':		{ 'symbol': '>=', 'operands': 2, 'priority': 500, 'function': lambda x, y: x >= y },
+	'btand':	{ 'symbol': '&',  'operands': 2, 'priority': 600, 'function': intf (operator.and_) },
+	'btxor':	{ 'symbol': '^',  'operands': 2, 'priority': 601, 'function': intf (operator.xor) },
+	'btor':		{ 'symbol': '|',  'operands': 2, 'priority': 602, 'function': intf (operator.or_) },
+	'and':		{ 'symbol': '&&', 'operands': 2, 'priority': 603, 'function': lambda x, y: x and y },
+	'or':		{ 'symbol': '||', 'operands': 2, 'priority': 604, 'function': lambda x, y: x or y },
+}
+
+for name, data in OperatorData.iteritems():
+	Operators.append (Operator (name=name, symbol=data['symbol'], operands=data['operands'],
+		priority=data['priority'], function=data['function']))
+
+OperatorSymbols={}
+for op in Operators:
+	if op.symbol in OperatorSymbols:
+		OperatorSymbols[op.symbol].append (op)
+	else:
+		OperatorSymbols[op.symbol] = [op]
+
+Constants = {
+	'pi': cmath.pi,
+	'e': cmath.e,
+	'phi': 1.6180339887498948482,
+	'epsilon': epsilon,
+}
+
+random.seed (time.time())
+
+Functions = {
+	'round':	{ 'function': realf (round) },
+	'floor':	{ 'function': realf (math.floor) },
+	'ceil':		{ 'function': realf (math.ceil) },
+	'fact':		{ 'function': intf (math.factorial) },
+	'abs':		{ 'function': realf (math.fabs) },
+	'degrees':	{ 'function': realf (math.degrees) },
+	'radians':	{ 'function': realf (math.radians) },
+	'erf':		{ 'function': realf (math.erf) },
+	'erfc':		{ 'function': realf (math.erfc) },
+	'gamma':	{ 'function': realf (math.gamma) },
+	'lgamma':	{ 'function': realf (math.lgamma) },
+	'sqrt':		{ 'function': cmath.sqrt },
+	'ln':		{ 'function': cmath.log },
+	'log':		{ 'function': cmath.log10 },
+	'sin':		{ 'function': cmath.sin },
+	'sinh':		{ 'function': cmath.sinh },
+	'asin':		{ 'function': cmath.asin },
+	'asinh':	{ 'function': cmath.asinh },
+	'cos':		{ 'function': cmath.cos },
+	'cosh':		{ 'function': cmath.cosh },
+	'acos':		{ 'function': cmath.acos },
+	'acosh':	{ 'function': cmath.acosh },
+	'tan':		{ 'function': cmath.tan },
+	'tanh':		{ 'function': cmath.tanh },
+	'atan':		{ 'function': cmath.atan },
+	'atanh':	{ 'function': cmath.atanh },
+	'exp':		{ 'function': cmath.exp },
+	'phase':	{ 'function': cmath.phase },
+	'lg':		{ 'function': lambda x: cmath.log (x, 2) },
+	're':		{ 'function': lambda x: x.real },
+	'im':		{ 'function': lambda x: x.imag },
+	'conjugate':{ 'function': lambda x: x.conjugate() },
+	'rand':		{ 'function': random.random },
+}
+
+Tokens = ['(', ')']
+
+# Symbol table
+SymbolType = Enum ('CONSTANT', 'FUNCTION', 'OPERATOR', 'TOKEN')
+SymbolTypes = {}
+Symbols = []
+
+for name, value in Constants.iteritems():
+	Symbols.append (name)
+	SymbolTypes[name] = SymbolType.CONSTANT
+
+for name, data in Functions.iteritems():
+	Symbols.append (name)
+	SymbolTypes[name] = SymbolType.FUNCTION
+
+for op in Operators:
+	if op.symbol not in Symbols:
+		Symbols.append (op.symbol)
+	SymbolTypes[op.symbol] = SymbolType.OPERATOR
+
+for name in Tokens:
+	SymbolTypes[name] = SymbolType.TOKEN
+
+Symbols += Tokens
+Symbols = sorted (Symbols, key=lambda x: len (x), reverse=True)
+
+def parse_number (expr):
+	"""Tries to parse a number from the expression. Returns (value, length) on success."""
+	i = 0
+	value = None
+
+	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):
+	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
+
+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)
+
+	# If there are sub-expressions in here, those need to be evaluated first
+	i = 0
+	while i < len (expr):
+		sym = expr[i]
+
+		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])
+
+		# 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]
+
+			printFunc (tabs + 'Evaluating function call: %s' % (sym))
+			expr[i] = Functions[sym.funcname]['function'] (*sym.args)
+			printFunc (tabs + '-> %s' % expr[i])
+			
+		i += 1
+
+	printFunc (tabs + 'Evaluate: %s' % expr)
+	runaway = 0
+	while True:
+		runaway += 1
+		if runaway > 1000:
+			raise ValueError ('infinite loop detected')
+
+		op, i = find_priority_operator (expr)
+		if not op:
+			break
+
+		if op.operands == 2:
+			argIndices = [i - 1, i + 1]
+		else:
+			argIndices = [i + 1]
+
+		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])
+
+		for i2 in argIndices:
+			del expr[i2]
+
+	printFunc (tabs + 'Result: %s' % expr[0])
+
+	if len(expr) != 1:
+		printFunc (tabs + 'Bad expression detected, tokens: %s' % expr)
+		raise ValueError ('malformed expression')
+
+	return expr[0]
+
+def repr_number (x):
+	"""Returns a string representation for a real number"""
+	rep = '%.10f' % x
+
+	if '.' in rep:
+		while rep[-1] == '0':
+			rep = rep[:-1]
+
+		if rep[-1] == '.':
+			rep = rep[:-1]
+
+	return rep
+
+def repr_imaginary (x):
+	rep = repr_number (x)
+
+	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)))
+		else:
+			# Pure imaginary number
+			return repr_imaginary (x.imag)
+	else:
+		# Real number
+		return 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))

mercurial