Skip to content

Commit

Permalink
Tidying up pyparsing
Browse files Browse the repository at this point in the history
  • Loading branch information
jolyonb committed Jul 5, 2018
1 parent c358c71 commit e56a40d
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 48 deletions.
69 changes: 22 additions & 47 deletions mitxgraders/helpers/calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
delimitedList
)
from mitxgraders.helpers.validatorfuncs import get_number_of_args
from mitxgraders.helpers.mathfunc import (DEFAULT_FUNCTIONS,
DEFAULT_SUFFIXES,
DEFAULT_VARIABLES)

class CalcError(Exception):
"""Base class for errors originating in calc.py"""
Expand Down Expand Up @@ -164,7 +167,11 @@ def get_parser(self, formula, suffixes):
# The global parser cache
parsercache = ParserCache()

def evaluator(formula, variables, functions, suffixes, allow_vectors=False):
def evaluator(formula,
variables=DEFAULT_VARIABLES,
functions=DEFAULT_FUNCTIONS,
suffixes=DEFAULT_SUFFIXES,
allow_vectors=False):
"""
Evaluate an expression; that is, take a string of math and return a float.
Expand Down Expand Up @@ -241,16 +248,16 @@ def __init__(self, math_expr, suffixes):
self.suffixes = suffixes
self.actions = {
'number': self.eval_number,
'variable': self.eval_var,
'arguments': self.eval_arguments,
'variable': lambda tokens: self.vars[tokens[0]],
'arguments': lambda tokens: tokens,
'function': self.eval_function,
'vector': self.eval_vector,
'power': self.eval_power,
'negation': self.eval_negation,
'parallel': self.eval_parallel,
'product': self.eval_product,
'sum': self.eval_sum,
'parentheses': lambda tokens: tokens[0] # just get the unique child
'parentheses': lambda tokens: tokens[0] # just get the unique child
}
self.vars = {}
self.functions = {}
Expand Down Expand Up @@ -325,8 +332,8 @@ def parse_algebra(self):
inner_number +
Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part),
adjacent=False
)
+ Optional(suffix)
)("num")
+ Optional(suffix)("suffix")
)("number")
# Note that calling ("name") on the end of a parser is equivalent to calling
# parser.setResultsName, which is used to pulling that result out of a parsed
Expand Down Expand Up @@ -357,7 +364,7 @@ def parse_algebra(self):
) +
ZeroOrMore("'"))
# Define a variable as a pyparsing result that contains one object name
variable = Group(name)("variable")
variable = Group(name("varname"))("variable")
variable.setParseAction(self.variable_parse_action)

# Predefine recursive variable expr
Expand All @@ -367,11 +374,11 @@ def parse_algebra(self):
# funcname(arguments)
# where arguments is a comma-separated list of arguments, returned as a list
# Must have at least 1 argument
function = Group(name +
function = Group(name("funcname") +
Suppress("(") +
Group(delimitedList(expr))("arguments") +
Suppress(")")
)("function")
)("function")
function.setParseAction(self.function_parse_action)

# Define parentheses
Expand All @@ -391,13 +398,13 @@ def parse_algebra(self):

# The following are in order of operational precedence
# Define exponentiation, possibly including negative powers
power = atom + ZeroOrMore(Suppress("^") + ZeroOrMore(minus) + atom)
power = atom + ZeroOrMore(Suppress("^") + Optional(minus)("op") + atom)
power.addParseAction(self.group_if_multiple('power'))

# Define negation (eg, in 5*-3 --> we need to evaluate the -3 first)
# Negation in powers is handled separately
# This has been arbitrarily assigned a higher precedence than parallel
negation = ZeroOrMore(minus) + power
negation = Optional(minus)("op") + power
negation.addParseAction(self.group_if_multiple('negation'))

# Define the parallel operator 1 || 5 == 1/(1/1 + 1/5)
Expand All @@ -406,12 +413,12 @@ def parse_algebra(self):
parallel.addParseAction(self.group_if_multiple('parallel'))

# Define multiplication and division
product = parallel + ZeroOrMore((Literal('*') | Literal('/')) + parallel)
product = parallel + ZeroOrMore((Literal('*') | Literal('/'))("op") + parallel)
product.addParseAction(self.group_if_multiple('product'))

# Define sums and differences
# Note that leading - signs are treated by negation
sumdiff = Optional(plus) + product + ZeroOrMore(plus_minus + product)
sumdiff = Optional(plus) + product + ZeroOrMore(plus_minus("op") + product)
sumdiff.addParseAction(self.group_if_multiple('sum'))

# Close the recursion
Expand All @@ -420,10 +427,9 @@ def parse_algebra(self):
# Save the resulting tree
self.tree = (expr + stringEnd).parseString(self.math_expr)[0]

@staticmethod
def dump_parse_result(parse_result): # pragma: no cover
def dump_parse_result(self): # pragma: no cover
"""Pretty-print an XML version of the parse_result for debug purposes"""
print(parse_result.asXML())
print(self.tree.asXML())

def set_vars_funcs(self, variables=None, functions=None):
"""Stores the given dictionaries of variables and functions for future use"""
Expand Down Expand Up @@ -526,37 +532,6 @@ def eval_number(self, parse_result):
result *= self.suffixes[parse_result[1]]
return result

def eval_var(self, parse_result):
"""
Return the value of the given variable.
Arguments:
parse_result: A list, [varname]
Usage
=====
>>> parser = FormulaParser("1", {"%": 0.01})
>>> parser.set_vars_funcs(variables={"x": 1})
>>> parser.eval_var(['x'])
1
"""
return self.vars[parse_result[0]]

def eval_arguments(self, parse_result):
"""
A wrapper that returns parse_result. Used for `arguments`.
Arguments:
parse_result: A list of function arguments
Usage
=====
>>> parser = FormulaParser("1", {"%": 0.01})
>>> parser.eval_arguments(['a', 'b', 'c'])
['a', 'b', 'c']
"""
return parse_result

def eval_function(self, parse_result):
"""
Evaluates a function
Expand Down
1 change: 0 additions & 1 deletion mitxgraders/helpers/mathfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import math
import numpy as np
import scipy.special as special
from mitxgraders.baseclasses import ConfigError

# Normal Trig
def sec(arg):
Expand Down
18 changes: 18 additions & 0 deletions tests/helpers/test_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import division
import math
import random
import re
import numpy as np
from pytest import raises, approx
from mitxgraders import CalcError
Expand Down Expand Up @@ -126,3 +127,20 @@ def test_vectors():
msg = "Vector and matrix expressions have been forbidden in this entry"
with raises(UnableToParse, match=msg):
evaluator("[[1, 2], [3, 4]]", {}, {}, {})

def test_negation():
"""Test that appropriate numbers of +/- signs are accepted"""
assert evaluator("1+-1")[0] == 0
assert evaluator("1--1")[0] == 2
assert evaluator("2*-1")[0] == -2
assert evaluator("2/-4")[0] == -0.5
assert evaluator("-1+-1")[0] == -2
assert evaluator("+1+-1")[0] == 0
assert evaluator("2^-2")[0] == 0.25
assert evaluator("+-1")[0] == -1

msg = "Invalid Input: Could not parse '{}' as a formula"
badformulas = ["1---1", "2^--2", "1-+2", "1++2", "1+++2", "--2", "---2", "-+2", "--+2"]
for formula in badformulas:
with raises(UnableToParse, match=re.escape(msg.format(formula))):
evaluator(formula)

0 comments on commit e56a40d

Please sign in to comment.