Skip to content

Commit

Permalink
Fix conflict between '!' and '!=' operators; add tests for comparison…
Browse files Browse the repository at this point in the history
… operators, including epsilon is_close checks; some PEP8 cleanup and warnings suppression
  • Loading branch information
ptmcg committed Feb 10, 2020
1 parent db336cf commit 1f60027
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 25 deletions.
66 changes: 41 additions & 25 deletions plusminus/plusminus.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,30 @@
expressions.update(keywords)

any_keyword = pp.MatchFirst(keywords.values())
# noinspection PyUnresolvedReferences
IN_RANGE_FROM = (IN + RANGE + FROM).addParseAction('_'.join)
# noinspection PyUnresolvedReferences
TRUE.addParseAction(lambda: True)
# noinspection PyUnresolvedReferences
FALSE.addParseAction(lambda: False)

FunctionSpec = namedtuple("FunctionSpec", "method arity")

_numeric_type = (int, float, complex)

# define special versions of lt, le, etc. to comprehend "is close"
_lt = lambda a, b, eps: a < b and not math.isclose(a, b, abs_tol=eps) if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a < b
_le = lambda a, b, eps: a <= b or math.isclose(a, b, abs_tol=eps) if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a <= b
_gt = lambda a, b, eps: a > b and not math.isclose(a, b, abs_tol=eps) if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a > b
_ge = lambda a, b, eps: a >= b or math.isclose(a, b, abs_tol=eps) if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a >= b
_eq = lambda a, b, eps: a == b or math.isclose(a, b, abs_tol=eps) if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a == b
_ne = lambda a, b, eps: not math.isclose(a, b, abs_tol=eps) if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a != b
_lt = lambda a, b, eps: (a < b and not math.isclose(a, b, abs_tol=eps)
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a < b)
_le = lambda a, b, eps: (a <= b or math.isclose(a, b, abs_tol=eps)
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a <= b)
_gt = lambda a, b, eps: (a > b and not math.isclose(a, b, abs_tol=eps)
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a > b)
_ge = lambda a, b, eps: (a >= b or math.isclose(a, b, abs_tol=eps)
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a >= b)
_eq = lambda a, b, eps: (a == b or math.isclose(a, b, abs_tol=eps)
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a == b)
_ne = lambda a, b, eps: (not math.isclose(a, b, abs_tol=eps)
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a != b)


@contextmanager
Expand Down Expand Up @@ -111,7 +120,7 @@ def collapse_operands(seq, eps=1e-15):
if cur[i] == 0:
# print(i, cur)
if cur[i+1] < 0 and (i == len(cur)-2 or cur[i+2] % 2 != 0):
0 ** cur[i+1]
unused = 0 ** cur[i+1]
else:
cur[i - 2:] = [1]
break
Expand Down Expand Up @@ -167,7 +176,7 @@ def safe_str_mult(a, b):
if isinstance(a, str):
if b <= 0:
return ''
if len(a) * abs(b) > 1e7:
if len(a) * abs(b) > 1e7:
raise MemoryError("expression creates too large a string")
a, b = b, a
return a * b
Expand Down Expand Up @@ -200,7 +209,7 @@ def left_associative_evaluate(self, oper_fn_map):

def __repr__(self):
return type(self).__name__ + '/' + (", ".join(repr(t) for t in self.tokens)
if self.iterable_tokens else repr(self.tokens))
if self.iterable_tokens else repr(self.tokens))


class LiteralNode(ArithNode):
Expand Down Expand Up @@ -248,6 +257,8 @@ def __repr__(self):


class TernaryNode(ArithNode):
opns_map = {}

def left_associative_evaluate(self, oper_fn_map):
operands = self.tokens
ret = operands[0].evaluate()
Expand Down Expand Up @@ -342,6 +353,7 @@ def evaluate(self):

class ArithmeticUnaryPostOp(UnaryNode):
opns_map = {}

def evaluate(self):
with _trimming_exception_traceback():
return self.left_associative_evaluate(self.opns_map)
Expand Down Expand Up @@ -411,7 +423,7 @@ def __init__(self):
self._added_operator_specs = []
self._added_function_specs = {}
self._base_operators = ("** * / mod × ÷ + - < > <= >= == != ≠ ≤ ≥ between-and within-and"
" in-range-from-to not and ∧ or ∨ ?:").split()
" in-range-from-to not and ∧ or ∨ ?:").split()
self._base_function_map = {
'sgn': FunctionSpec((lambda x: -1 if x < 0 else 1 if x > 0 else 0), 1),
'abs': FunctionSpec(abs, 1),
Expand Down Expand Up @@ -484,19 +496,16 @@ def customize(self):
pass

def add_operator(self, operator_expr, arity, assoc, parse_action):
if isinstance(operator_expr, str) and callable(parse_action):
operator_node_superclass = {
(1, pp.opAssoc.LEFT): self.ArithmeticUnaryPostOp,
(1, pp.opAssoc.RIGHT): self.ArithmeticUnaryOp,
(2, pp.opAssoc.LEFT): self.ArithmeticBinaryOp,
(2, pp.opAssoc.RIGHT): self.ArithmeticBinaryOp,
(3, pp.opAssoc.LEFT): TernaryNode,
(3, pp.opAssoc.RIGHT): TernaryNode,
}[arity, assoc]
operator_node_class = type('', (operator_node_superclass,),
{'opns_map': {operator_expr: parse_action}})
else:
operator_node_class = parse_action
operator_node_superclass = {
(1, pp.opAssoc.LEFT): self.ArithmeticUnaryPostOp,
(1, pp.opAssoc.RIGHT): self.ArithmeticUnaryOp,
(2, pp.opAssoc.LEFT): self.ArithmeticBinaryOp,
(2, pp.opAssoc.RIGHT): self.ArithmeticBinaryOp,
(3, pp.opAssoc.LEFT): TernaryNode,
(3, pp.opAssoc.RIGHT): TernaryNode,
}[arity, assoc]
operator_node_class = type('', (operator_node_superclass,),
{'opns_map': {str(operator_expr): parse_action}})
self._added_operator_specs.insert(0, (operator_expr, arity, assoc, operator_node_class))

def initialize_variable(self, vname, vvalue, as_formula=False):
Expand Down Expand Up @@ -637,6 +646,7 @@ def evaluate(self):

identifier_node_class = type('Identifier', (self.IdentifierNode,), {'_assigned_vars': self._variable_map})
var_name.addParseAction(identifier_node_class)
# noinspection PyUnresolvedReferences
base_operator_specs = [
('**', 2, pp.opAssoc.LEFT, self.ExponentBinaryOp),
('-', 1, pp.opAssoc.RIGHT, self.ArithmeticUnaryOp),
Expand All @@ -652,11 +662,13 @@ def evaluate(self):
]
ABS_VALUE_VERT = pp.Suppress("|")
abs_value_expression = ABS_VALUE_VERT + arith_operand + ABS_VALUE_VERT

def cvt_to_function_call(tokens):
ret = pp.ParseResults(['abs']) + tokens
ret['fn_name'] = 'abs'
ret['args'] = tokens
return [ret]

abs_value_expression.addParseAction(cvt_to_function_call, function_node_class)

arith_operand <<= pp.infixNotation((function_expression
Expand All @@ -670,7 +682,9 @@ def cvt_to_function_call(tokens):
rvalue.setName("arithmetic expression")
lvalue = var_name()

value_assignment_statement = pp.delimitedList(lvalue)("lhs") + pp.oneOf("<- =") + pp.delimitedList(rvalue)("rhs")
value_assignment_statement = (pp.delimitedList(lvalue)("lhs")
+ pp.oneOf("<- =")
+ pp.delimitedList(rvalue)("rhs"))

def eval_and_store_value(tokens):
if len(tokens.lhs) > len(tokens.rhs):
Expand Down Expand Up @@ -765,7 +779,9 @@ def customize(self):
self.add_function('rnd', 0, random.random)
self.add_function('randint', 2, random.randint)
self.add_operator('°', 1, ArithmeticParser.LEFT, math.radians)
self.add_operator("!", 1, ArithmeticParser.LEFT, constrained_factorial)
# avoid clash with '!=' operator
factorial_operator = (~pp.Literal("!=") + "!").setName("!")
self.add_operator(factorial_operator, 1, ArithmeticParser.LEFT, constrained_factorial)
self.add_operator("⁻¹", 1, ArithmeticParser.LEFT, lambda x: 1 / x)
self.add_operator("²", 1, ArithmeticParser.LEFT, lambda x: safe_pow((x, 2)))
self.add_operator("³", 1, ArithmeticParser.LEFT, lambda x: safe_pow((x, 3)))
Expand Down
21 changes: 21 additions & 0 deletions test/arith_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@
(11 between 10 and 15) == (10 < 11 < 15)
32 + 37 * 9 / 5 == 98.6
ctemp = 37
temp_f = 100.2
temp_f > 98.6 ? "fever" : "normal"
"You " + (temp_f > 98.6 ? "have" : "don't have") + " a fever"
ctemp = 38
feverish @= temp_f > 98.6
"You " + (feverish ? "have" : "don't have") + " a fever"
temp_f = 98.2
"You " + (feverish ? "have" : "don't have") + " a fever"
a = 100
b @= a / 10
a / 2
Expand Down Expand Up @@ -83,6 +86,24 @@
0**(-1)**3
1000000000000**1000000000000**0
1000000000000**0**1000000000000**1000000000000
100 < 101
100 <= 101
100 > 101
100 >= 101
100 == 101
100 != 101
100 < 99
100 <= 99
100 > 99
100 >= 99
100 == 99
100 != 99
100 < 100+1E-18
100 <= 100+1E-18
100 > 100+1E-18
100 >= 100+1E-18
100 == 100+1E-18
100 != 100+1E-18
""",
postParse=lambda teststr, result: result[0].evaluate() if '@=' not in teststr else None)

Expand Down

0 comments on commit 1f60027

Please sign in to comment.