Major refactor: moved calculator routines into a Calculator class.

Sun, 19 Apr 2015 22:02:02 +0300

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Sun, 19 Apr 2015 22:02:02 +0300
changeset 128
bd949c554dd2
parent 127
66b206bd9510
child 129
8aa03b5c6e47

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

mercurial