def test_tokenize_trigonometrics(self): expression = "sin(5) + cos(5) + tan(5) + ctan(5)" token_list = [ tokens.SinFunctionToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(5), tokens.CloseParenthesisToken(), tokens.PlusOperatorToken(), tokens.CosFunctionToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(5), tokens.CloseParenthesisToken(), tokens.PlusOperatorToken(), tokens.TanFunctionToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(5), tokens.CloseParenthesisToken(), tokens.PlusOperatorToken(), tokens.CtanFunctionToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(5), tokens.CloseParenthesisToken() ] computed_token_list = tokenize(expression) self.assertListEqual(computed_token_list, token_list)
def test_tokenize_1(self): expression = "(3 + (4 - 1)) * 5" token_list = [ tokens.OpenParenthesisToken(), tokens.OperandToken(3), tokens.PlusOperatorToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(4), tokens.MinusOperatorToken(), tokens.OperandToken(1), tokens.CloseParenthesisToken(), tokens.CloseParenthesisToken(), tokens.ProductOperatorToken(), tokens.OperandToken(5) ] computed_token_list = tokenize(expression) self.assertListEqual(computed_token_list, token_list)
def test_tokenize_logarithm_with_custom_base(self): expr = "Log100(10)" token_list = [ tokens.LogFunctionToken(has_custom_base=True), tokens.OperandToken(100), tokens.OpenParenthesisToken(), tokens.OperandToken(10), tokens.CloseParenthesisToken() ] self.assertListEqual(token_list, tokenize(expr))
def test_tokenize_4(self): expression = "Log(10)" token_list = [ tokens.LogFunctionToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(10), tokens.CloseParenthesisToken() ] computed_token_list = tokenize(expression) self.assertListEqual(computed_token_list, token_list)
def test_tokenize_implicit_product_variable_and_parentheses(self): expr = "x(2-1)" token_list = [ tokens.VariableToken('x'), tokens.ProductOperatorToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(2), tokens.MinusOperatorToken(), tokens.OperandToken(1), tokens.CloseParenthesisToken() ] self.assertListEqual(token_list, tokenize(expr))
def test_tokenize_negative_within_parenthesis(self): expression = "(-5 + 2)" token_list = [ tokens.OpenParenthesisToken(), tokens.OperandToken(-5), tokens.PlusOperatorToken(), tokens.OperandToken(2), tokens.CloseParenthesisToken() ] computed_token_list = tokenize(expression) self.assertListEqual(computed_token_list, token_list)
def test_tokenize_7(self): expression = "sin(1.5*pi)" token_list = [ tokens.SinFunctionToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(1.5), tokens.ProductOperatorToken(), tokens.PiConstantToken(), tokens.CloseParenthesisToken() ] computed_token_list = tokenize(expression) self.assertListEqual(computed_token_list, token_list)
def test_tokenize_3(self): expression = "2x + 1 = 2(1-x)" token_list = [ tokens.OperandToken(2), tokens.ProductOperatorToken(), tokens.VariableToken('x'), tokens.PlusOperatorToken(), tokens.OperandToken(1), tokens.EqualSignToken(), tokens.OperandToken(2), tokens.ProductOperatorToken(), tokens.OpenParenthesisToken(), tokens.OperandToken(1), tokens.MinusOperatorToken(), tokens.VariableToken('x'), tokens.CloseParenthesisToken() ] computed_token_list = tokenize(expression) self.assertListEqual(computed_token_list, token_list)
def evaluate(input, is_postfix=False): input = input.strip() if not input: raise RuntimeError("Missing expression") logger.debug("Evaluating input '%s'", input) # Tokenize the input token_list = tokenize(input) logger.debug("Tokens:") for tok in token_list: logger.debug(" %r", tok) # In Postfix notation, make sure there are no parenthesis, variables or equal signs if is_postfix: for tok in token_list: if isinstance( tok, (tokens.OpenParenthesisToken, tokens.CloseParenthesisToken)): raise RuntimeError( "Parenthesis are not allowed in postfix expressions") if isinstance(tok, (tokens.EqualSignToken, tokens.VariableToken)): raise RuntimeError( "Equations cannot be written in postfix notation") # Decide whether it's an expression or an equation is_equation = False # For equations, convert the equal sign into a "minus" sign and surround the RHS of the equation in parenthesis for i, tok in enumerate(token_list): if isinstance(tok, tokens.EqualSignToken): logger.debug("Detected equation") is_equation = True token_list.pop(i) token_list.insert(i, tokens.MinusOperatorToken()) token_list.insert(i + 1, tokens.OpenParenthesisToken()) token_list.append(tokens.CloseParenthesisToken()) break # Equation testing if is_equation: # Make sure there are variables if it's an equation for i, tok in enumerate(token_list): if isinstance(tok, tokens.VariableToken): break else: raise RuntimeError("Incorrect equation: missing variable") # Make sure there are no more equal signs if len([x for x in token_list if isinstance(x, tokens.EqualSignToken)]): raise RuntimeError( "Incorrect equation: more than one equal sign found") # Make sure all variables are the same if len( set([ x.value for x in token_list if isinstance(x, tokens.VariableToken) ])) > 1: raise RuntimeError("Only one variable allowed") # Make sure there are no variables if it's not an equation if not is_equation: for i, tok in enumerate(token_list): if isinstance(tok, tokens.VariableToken): raise RuntimeError( "Variable '{}' found in non-equation".format(tok.value)) if not is_postfix: # Even if it's not postfix, run a dumb detection algorithm just to make sure for i, tok in enumerate(token_list[:-1]): if isinstance(tok, tokens.OperandToken) and isinstance( token_list[i + 1], tokens.OperandToken): logger.debug("Expression already in postfix") is_postfix = True break if not is_postfix: logger.debug("Converting to postfix") token_list = infix_to_postfix(token_list) logger.debug("Tokens in postfix:") for tok in token_list: logger.debug(" %r", tok) stack = [] logger.debug("Evaluating token list:") for tok in token_list: logger.debug("") logger.debug("Stack: %r", stack) logger.debug("%r", tok) node = None if tokens.is_operand(tok): node = EquationNode(constant=tok.value) elif tokens.is_variable(tok): node = EquationNode(coefficient=1) elif tokens.is_constant(tok): node = EquationNode(constant=tok.value) else: if tokens.is_operator(tok): try: second_operand = stack.pop() first_operand = stack.pop() except IndexError: raise RuntimeError("Bad expression, missing operands") logger.debug(" Oper A: %r", first_operand) logger.debug(" Oper B: %r", second_operand) if isinstance(tok, tokens.PlusOperatorToken): constant = None coefficient = None if first_operand.constant is not None: constant = first_operand.constant if second_operand.constant is not None: if constant is None: constant = 0 constant += second_operand.constant if first_operand.coefficient is not None: coefficient = first_operand.coefficient if second_operand.coefficient is not None: if coefficient is None: coefficient = 0 coefficient += second_operand.coefficient node = EquationNode(coefficient=coefficient, constant=constant) elif isinstance(tok, tokens.MinusOperatorToken): constant = None coefficient = None if first_operand.constant is not None: constant = first_operand.constant if second_operand.constant is not None: if constant is None: constant = 0 constant -= second_operand.constant if first_operand.coefficient is not None: coefficient = first_operand.coefficient if second_operand.coefficient is not None: if coefficient is None: coefficient = 0 coefficient -= second_operand.coefficient node = EquationNode(coefficient=coefficient, constant=constant) elif isinstance(tok, tokens.ProductOperatorToken): constant = None coefficient = None if first_operand.has_constant(): if second_operand.has_constant(): if constant is None: constant = 0 constant += first_operand.constant * second_operand.constant if second_operand.has_coef(): if coefficient is None: coefficient = 0 coefficient += first_operand.constant * second_operand.coefficient if first_operand.has_coef(): if second_operand.has_constant(): if coefficient is None: coefficient = 0 coefficient += first_operand.coefficient * second_operand.constant if second_operand.has_coef(): raise RuntimeError( "Unsupported expression, can't have exponential variables" ) node = EquationNode(coefficient=coefficient, constant=constant) elif isinstance(tok, tokens.DivisionOperatorToken): constant = None coefficient = None if first_operand.has_constant(): if second_operand.has_constant(): if constant is None: constant = 0 constant += first_operand.constant / second_operand.constant if second_operand.has_coef(): raise RuntimeError( "Unsupported expression, non-linear equations are not supported" ) if first_operand.has_coef(): if second_operand.has_constant(): if coefficient is None: coefficient = 0 coefficient += first_operand.coefficient / second_operand.constant if second_operand.has_coef(): raise RuntimeError( "Unsupported expression, can't have exponential variables" ) node = EquationNode(coefficient=coefficient, constant=constant) elif tokens.is_function(tok): try: operand = stack.pop() except IndexError as e: raise RuntimeError( "Missing value for trigonometric function") # Functions are only allowed in simple expressions, not in equations, so there should be no coeffs. if operand.has_coef(): raise RuntimeError("Function not allowed in equations") # Make sure there's a value to work with if not operand.has_constant(): raise RuntimeError( "Missing value for trigonometric function") if tokens.is_trigonometric(tok): node = EquationNode(constant=tok.oper(operand.constant)) elif isinstance(tok, tokens.LnFunctionToken): node = EquationNode( constant=math.log(operand.constant, math.e)) elif isinstance(tok, tokens.LogFunctionToken): base = 10 if tok.has_custom_base: base = stack.pop().constant node = EquationNode( constant=math.log(operand.constant, base)) if node is not None: logger.debug("Pushing to stack: %r", node) stack.append(node) logger.debug("Final stack %r", stack) arred = lambda x, n: x * (10**n) // 1 / (10**n) logger.info("Input: {}".format(input)) if is_equation: value = -stack[-1].constant / stack[-1].coefficient value = arred(value, 10) logger.info("x = {}".format(value)) else: value = stack[-1].constant value = arred(value, 10) logger.info(value) return value
def tokenize(input: str): input = input.lower() token_list = [] i = 0 while i < len(input): char = input[i] if char == '(': token_list.append(tokens.OpenParenthesisToken()) elif char == ')': token_list.append(tokens.CloseParenthesisToken()) elif char == '+': token_list.append(tokens.PlusOperatorToken()) # Only compute the '-' char as the minus operator if it's not part of a negative operand elif char == '-' and token_list and not isinstance(token_list[-1], tokens.OpenParenthesisToken): token_list.append(tokens.MinusOperatorToken()) elif char == '*': token_list.append(tokens.ProductOperatorToken()) elif char == '/': token_list.append(tokens.DivisionOperatorToken()) elif char == '=': token_list.append(tokens.EqualSignToken()) elif char.isalpha(): token_strings = { 'sin': tokens.SinFunctionToken, 'cos': tokens.CosFunctionToken, 'tan': tokens.TanFunctionToken, 'ctan': tokens.CtanFunctionToken, 'pi': tokens.PiConstantToken, 'e': tokens.EulerConstantToken, 'log': tokens.LogFunctionToken, 'ln': tokens.LnFunctionToken } for key in token_strings: if input[i:i + len(key)] == key: token_list.append(token_strings[key]()) i += len(key) - 1 break # The character was not part of any special token else: token_components = [char] j = i + 1 while j < len(input) and input[j].isalpha(): token_components.append(input[j]) j += 1 i = j - 1 token_list.append(tokens.VariableToken(''.join(token_components))) elif char.isdecimal() or char == '-': token_components = [char] # Keep consuming decimal characters j = i + 1 while j < len(input) and (input[j].isdecimal() or input[j] == '.'): token_components.append(input[j]) j += 1 i = j - 1 token_list.append(tokens.OperandToken(float(''.join(token_components)))) i += 1 # Token List postprocessing, in particular: # - Add product operator between operand and variable # - Add product operator between operand and constant # - Add product operator between operand and open parenthesis (except for the Log function) # - Add product operator between variable and open parenthesis # - Mark LogFunctionToken to have a custom base if followed by two ConstantTokens processed_token_list = [] for i, tok in enumerate(token_list): processed_token_list.append(tok) if i == len(token_list) - 1: break if is_operand(tok): # - Add product operator between operand and variable if is_variable(token_list[i + 1]): logger.debug("Adding implicit product operator between operand and variable") processed_token_list.append(tokens.ProductOperatorToken()) elif is_constant(token_list[i + 1]): logger.debug("Adding implicit product operator between operand and constant") processed_token_list.append(tokens.ProductOperatorToken()) # - Add product operator between operand and open parenthesis (except for the Log function) elif is_left_paren(token_list[i + 1]) and not isinstance(token_list[i - 1], tokens.LogFunctionToken): logger.debug("Adding implicit product operator between operand and open parenthesis") processed_token_list.append(tokens.ProductOperatorToken()) elif is_variable(tok): # - Add product operator between variable and open parenthesis if is_left_paren(token_list[i + 1]): logger.debug("Adding implicit product operator between variable and open parenthesis") processed_token_list.append(tokens.ProductOperatorToken()) # - Mark LogFunctionToken to have a custom base if followed by two ConstantTokens or a ConstantToken and open parenthesis elif isinstance(tok, tokens.LogFunctionToken) and i < len(token_list) - 2 and is_operand( token_list[i + 1]) and (is_constant(token_list[i + 2]) or is_left_paren(token_list[i + 2])): tok.has_custom_base = True return processed_token_list