Skip to content

Commit 1f60027

Browse files
committed
Fix conflict between '!' and '!=' operators; add tests for comparison operators, including epsilon is_close checks; some PEP8 cleanup and warnings suppression
1 parent db336cf commit 1f60027

File tree

2 files changed

+62
-25
lines changed

2 files changed

+62
-25
lines changed

plusminus/plusminus.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,30 @@
6666
expressions.update(keywords)
6767

6868
any_keyword = pp.MatchFirst(keywords.values())
69+
# noinspection PyUnresolvedReferences
6970
IN_RANGE_FROM = (IN + RANGE + FROM).addParseAction('_'.join)
71+
# noinspection PyUnresolvedReferences
7072
TRUE.addParseAction(lambda: True)
73+
# noinspection PyUnresolvedReferences
7174
FALSE.addParseAction(lambda: False)
7275

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

7578
_numeric_type = (int, float, complex)
7679

7780
# define special versions of lt, le, etc. to comprehend "is close"
78-
_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
79-
_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
80-
_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
81-
_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
82-
_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
83-
_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
81+
_lt = lambda a, b, eps: (a < b and not math.isclose(a, b, abs_tol=eps)
82+
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a < b)
83+
_le = lambda a, b, eps: (a <= b or math.isclose(a, b, abs_tol=eps)
84+
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a <= b)
85+
_gt = lambda a, b, eps: (a > b and not math.isclose(a, b, abs_tol=eps)
86+
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a > b)
87+
_ge = lambda a, b, eps: (a >= b or math.isclose(a, b, abs_tol=eps)
88+
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a >= b)
89+
_eq = lambda a, b, eps: (a == b or math.isclose(a, b, abs_tol=eps)
90+
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a == b)
91+
_ne = lambda a, b, eps: (not math.isclose(a, b, abs_tol=eps)
92+
if isinstance(a, _numeric_type) and isinstance(b, _numeric_type) else a != b)
8493

8594

8695
@contextmanager
@@ -111,7 +120,7 @@ def collapse_operands(seq, eps=1e-15):
111120
if cur[i] == 0:
112121
# print(i, cur)
113122
if cur[i+1] < 0 and (i == len(cur)-2 or cur[i+2] % 2 != 0):
114-
0 ** cur[i+1]
123+
unused = 0 ** cur[i+1]
115124
else:
116125
cur[i - 2:] = [1]
117126
break
@@ -167,7 +176,7 @@ def safe_str_mult(a, b):
167176
if isinstance(a, str):
168177
if b <= 0:
169178
return ''
170-
if len(a) * abs(b) > 1e7:
179+
if len(a) * abs(b) > 1e7:
171180
raise MemoryError("expression creates too large a string")
172181
a, b = b, a
173182
return a * b
@@ -200,7 +209,7 @@ def left_associative_evaluate(self, oper_fn_map):
200209

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

205214

206215
class LiteralNode(ArithNode):
@@ -248,6 +257,8 @@ def __repr__(self):
248257

249258

250259
class TernaryNode(ArithNode):
260+
opns_map = {}
261+
251262
def left_associative_evaluate(self, oper_fn_map):
252263
operands = self.tokens
253264
ret = operands[0].evaluate()
@@ -342,6 +353,7 @@ def evaluate(self):
342353

343354
class ArithmeticUnaryPostOp(UnaryNode):
344355
opns_map = {}
356+
345357
def evaluate(self):
346358
with _trimming_exception_traceback():
347359
return self.left_associative_evaluate(self.opns_map)
@@ -411,7 +423,7 @@ def __init__(self):
411423
self._added_operator_specs = []
412424
self._added_function_specs = {}
413425
self._base_operators = ("** * / mod × ÷ + - < > <= >= == != ≠ ≤ ≥ between-and within-and"
414-
" in-range-from-to not and ∧ or ∨ ?:").split()
426+
" in-range-from-to not and ∧ or ∨ ?:").split()
415427
self._base_function_map = {
416428
'sgn': FunctionSpec((lambda x: -1 if x < 0 else 1 if x > 0 else 0), 1),
417429
'abs': FunctionSpec(abs, 1),
@@ -484,19 +496,16 @@ def customize(self):
484496
pass
485497

486498
def add_operator(self, operator_expr, arity, assoc, parse_action):
487-
if isinstance(operator_expr, str) and callable(parse_action):
488-
operator_node_superclass = {
489-
(1, pp.opAssoc.LEFT): self.ArithmeticUnaryPostOp,
490-
(1, pp.opAssoc.RIGHT): self.ArithmeticUnaryOp,
491-
(2, pp.opAssoc.LEFT): self.ArithmeticBinaryOp,
492-
(2, pp.opAssoc.RIGHT): self.ArithmeticBinaryOp,
493-
(3, pp.opAssoc.LEFT): TernaryNode,
494-
(3, pp.opAssoc.RIGHT): TernaryNode,
495-
}[arity, assoc]
496-
operator_node_class = type('', (operator_node_superclass,),
497-
{'opns_map': {operator_expr: parse_action}})
498-
else:
499-
operator_node_class = parse_action
499+
operator_node_superclass = {
500+
(1, pp.opAssoc.LEFT): self.ArithmeticUnaryPostOp,
501+
(1, pp.opAssoc.RIGHT): self.ArithmeticUnaryOp,
502+
(2, pp.opAssoc.LEFT): self.ArithmeticBinaryOp,
503+
(2, pp.opAssoc.RIGHT): self.ArithmeticBinaryOp,
504+
(3, pp.opAssoc.LEFT): TernaryNode,
505+
(3, pp.opAssoc.RIGHT): TernaryNode,
506+
}[arity, assoc]
507+
operator_node_class = type('', (operator_node_superclass,),
508+
{'opns_map': {str(operator_expr): parse_action}})
500509
self._added_operator_specs.insert(0, (operator_expr, arity, assoc, operator_node_class))
501510

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

638647
identifier_node_class = type('Identifier', (self.IdentifierNode,), {'_assigned_vars': self._variable_map})
639648
var_name.addParseAction(identifier_node_class)
649+
# noinspection PyUnresolvedReferences
640650
base_operator_specs = [
641651
('**', 2, pp.opAssoc.LEFT, self.ExponentBinaryOp),
642652
('-', 1, pp.opAssoc.RIGHT, self.ArithmeticUnaryOp),
@@ -652,11 +662,13 @@ def evaluate(self):
652662
]
653663
ABS_VALUE_VERT = pp.Suppress("|")
654664
abs_value_expression = ABS_VALUE_VERT + arith_operand + ABS_VALUE_VERT
665+
655666
def cvt_to_function_call(tokens):
656667
ret = pp.ParseResults(['abs']) + tokens
657668
ret['fn_name'] = 'abs'
658669
ret['args'] = tokens
659670
return [ret]
671+
660672
abs_value_expression.addParseAction(cvt_to_function_call, function_node_class)
661673

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

673-
value_assignment_statement = pp.delimitedList(lvalue)("lhs") + pp.oneOf("<- =") + pp.delimitedList(rvalue)("rhs")
685+
value_assignment_statement = (pp.delimitedList(lvalue)("lhs")
686+
+ pp.oneOf("<- =")
687+
+ pp.delimitedList(rvalue)("rhs"))
674688

675689
def eval_and_store_value(tokens):
676690
if len(tokens.lhs) > len(tokens.rhs):
@@ -765,7 +779,9 @@ def customize(self):
765779
self.add_function('rnd', 0, random.random)
766780
self.add_function('randint', 2, random.randint)
767781
self.add_operator('°', 1, ArithmeticParser.LEFT, math.radians)
768-
self.add_operator("!", 1, ArithmeticParser.LEFT, constrained_factorial)
782+
# avoid clash with '!=' operator
783+
factorial_operator = (~pp.Literal("!=") + "!").setName("!")
784+
self.add_operator(factorial_operator, 1, ArithmeticParser.LEFT, constrained_factorial)
769785
self.add_operator("⁻¹", 1, ArithmeticParser.LEFT, lambda x: 1 / x)
770786
self.add_operator("²", 1, ArithmeticParser.LEFT, lambda x: safe_pow((x, 2)))
771787
self.add_operator("³", 1, ArithmeticParser.LEFT, lambda x: safe_pow((x, 3)))

test/arith_tests.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@
3737
(11 between 10 and 15) == (10 < 11 < 15)
3838
32 + 37 * 9 / 5 == 98.6
3939
ctemp = 37
40+
temp_f = 100.2
4041
temp_f > 98.6 ? "fever" : "normal"
4142
"You " + (temp_f > 98.6 ? "have" : "don't have") + " a fever"
4243
ctemp = 38
4344
feverish @= temp_f > 98.6
4445
"You " + (feverish ? "have" : "don't have") + " a fever"
46+
temp_f = 98.2
47+
"You " + (feverish ? "have" : "don't have") + " a fever"
4548
a = 100
4649
b @= a / 10
4750
a / 2
@@ -83,6 +86,24 @@
8386
0**(-1)**3
8487
1000000000000**1000000000000**0
8588
1000000000000**0**1000000000000**1000000000000
89+
100 < 101
90+
100 <= 101
91+
100 > 101
92+
100 >= 101
93+
100 == 101
94+
100 != 101
95+
100 < 99
96+
100 <= 99
97+
100 > 99
98+
100 >= 99
99+
100 == 99
100+
100 != 99
101+
100 < 100+1E-18
102+
100 <= 100+1E-18
103+
100 > 100+1E-18
104+
100 >= 100+1E-18
105+
100 == 100+1E-18
106+
100 != 100+1E-18
86107
""",
87108
postParse=lambda teststr, result: result[0].evaluate() if '@=' not in teststr else None)
88109

0 commit comments

Comments
 (0)