From e0755b5014a818f80fe8aabc50d58c0bbe923584 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Wed, 15 Jan 2025 12:09:47 -0500 Subject: [PATCH] Add Between, correct some types... (#1282) Some flycheck linting done on the `mathics.core.testing_expressions.equality_inequality` module. Between is used the Biology, Chemistry, and CodeInspector packages among others. --------- Co-authored-by: Juan Mauricio Matera --- CHANGES.rst | 1 + .../equality_inequality.py | 198 ++++++++++++------ mathics/core/builtin.py | 6 +- test/test_main_preeval.py | 4 +- 4 files changed, 140 insertions(+), 69 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5cf0e58c6..53a8997ee 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ CHANGES New Builtins ++++++++++++ +* ``Between`` * ``CheckAbort`` * ``FileNameDrop`` * ``SetEnvironment`` diff --git a/mathics/builtin/testing_expressions/equality_inequality.py b/mathics/builtin/testing_expressions/equality_inequality.py index 542c7d685..40d1def63 100644 --- a/mathics/builtin/testing_expressions/equality_inequality.py +++ b/mathics/builtin/testing_expressions/equality_inequality.py @@ -3,12 +3,13 @@ Equality and Inequality """ -from typing import Any, Optional +from abc import ABC +from typing import Any, Optional, Union import sympy from mathics.builtin.numbers.constants import mp_convert_constant -from mathics.core.atoms import COMPARE_PREC, Integer, Integer1, Number, String +from mathics.core.atoms import COMPARE_PREC, Number, String from mathics.core.attributes import ( A_FLAT, A_NUMERIC_FUNCTION, @@ -17,24 +18,18 @@ A_PROTECTED, ) from mathics.core.builtin import Builtin, InfixOperator, SympyFunction -from mathics.core.convert.expression import to_expression, to_numeric_args +from mathics.core.convert.expression import to_numeric_args +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.expression_predefined import ( - MATHICS3_COMPLEX_INFINITY, - MATHICS3_INFINITY, - MATHICS3_NEG_INFINITY, -) +from mathics.core.expression_predefined import MATHICS3_INFINITY, MATHICS3_NEG_INFINITY from mathics.core.number import dps -from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolList, SymbolTrue +from mathics.core.symbols import Symbol, SymbolFalse, SymbolList, SymbolTrue from mathics.core.systemsymbols import ( SymbolAnd, SymbolDirectedInfinity, SymbolExactNumberQ, SymbolInequality, - SymbolInfinity, SymbolMaxExtraPrecision, - SymbolMaxPrecision, - SymbolSign, ) from mathics.eval.nevaluator import eval_N from mathics.eval.numerify import numerify @@ -50,21 +45,29 @@ } -class _InequalityOperator(InfixOperator): +class _InequalityOperator(InfixOperator, ABC): + """ + A class for builtin functions with element inequality + comparisons in a chain e.g. a != b != c compares a != b and b != + c. + """ + grouping = "NonAssociative" @staticmethod - def numerify_args(items, evaluation) -> list: - items_sequence = items.get_sequence() + def numerify_args(elements, evaluation: Evaluation) -> Union[list, tuple]: + element_sequence = elements.get_sequence() all_numeric = all( item.is_numeric(evaluation) and item.get_precision() is None - for item in items_sequence + for item in element_sequence ) # All expressions are numeric but exact and they are not all numbers, - if all_numeric and any(not isinstance(item, Number) for item in items_sequence): + if all_numeric and any( + not isinstance(item, Number) for item in element_sequence + ): # so apply N and compare them. - items = items_sequence + items = element_sequence n_items = [] for item in items: if not isinstance(item, Number): @@ -72,27 +75,30 @@ def numerify_args(items, evaluation) -> list: n_items.append(item) items = n_items else: - items = to_numeric_args(items, evaluation) + items = to_numeric_args(elements, evaluation) return items -class _ComparisonOperator(_InequalityOperator): - "Compares arguments in a chain e.g. a < b < c compares a < b and b < c." +class _ComparisonOperator(_InequalityOperator, ABC): + """ + A class for builtin functions with element comparisons in a + chain e.g. a < b < c compares a < b and b < c. + """ - def eval(self, items, evaluation): - "%(name)s[items___]" - items_sequence = items.get_sequence() - if len(items_sequence) <= 1: + def eval(self, elements, evaluation: Evaluation): + "%(name)s[elements___]" + elements_sequence = elements.get_sequence() + if len(elements_sequence) <= 1: return SymbolTrue - items = self.numerify_args(items, evaluation) + elements = self.numerify_args(elements, evaluation) wanted = operators[self.get_name()] - if isinstance(items[-1], String): + if isinstance(elements[-1], String): return None - for i in range(len(items) - 1): - x = items[i] + for i in range(len(elements) - 1): + x = elements[i] if isinstance(x, String): return None - y = items[i + 1] + y = elements[i + 1] c = do_cmp(x, y) if c is None: return @@ -102,8 +108,11 @@ def eval(self, items, evaluation): return SymbolTrue -class _EqualityOperator(_InequalityOperator): - "Compares all pairs e.g. a == b == c compares a == b, b == c, and a == c." +class _EqualityOperator(_InequalityOperator, ABC): + """ + A class for builtin functions with element equality in a + chain e.g. a == b == c compares a == b and b == c. + """ @staticmethod def get_pairs(args): @@ -151,6 +160,15 @@ def infty_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]: # DirectedInfinity with more than two elements cannot be compared here... return None + # Below, we default to the method used for equality builtin functions. + # Inequality builtin functions will redefine this method. + @staticmethod + def operator_sense(value) -> bool: + """function used to check whether `value` is the right Boolean-valued + sense needed for a particluar equality or inequality builtin function. + """ + return bool(value) + def sympy_equal(self, lhs, rhs, max_extra_prec=None) -> Optional[bool]: try: lhs_sympy = lhs.to_sympy(evaluate=True, prec=COMPARE_PREC) @@ -209,28 +227,28 @@ def equal2(self, lhs: Any, rhs: Any, max_extra_prec=None) -> Optional[bool]: return c return None - def eval(self, items, evaluation): - "%(name)s[items___]" - items_sequence = items.get_sequence() - n = len(items_sequence) + def eval(self, elements, evaluation: Evaluation): + "%(name)s[elements___]" + elements_sequence = elements.get_sequence() + n = len(elements_sequence) if n <= 1: return SymbolTrue is_exact_vals = [ Expression(SymbolExactNumberQ, arg).evaluate(evaluation) - for arg in items_sequence + for arg in elements_sequence ] if not all(val is SymbolTrue for val in is_exact_vals): - return self.eval_other(items, evaluation) - args = self.numerify_args(items, evaluation) + return self.eval_other(elements, evaluation) + args = self.numerify_args(elements, evaluation) for x, y in self.get_pairs(args): c = do_cplx_equal(x, y) if c is None: return - if not self._op(c): + if not self.operator_sense(c): return SymbolFalse return SymbolTrue - def eval_other(self, args, evaluation): + def eval_other(self, args, evaluation: Evaluation): "%(name)s[args___?(!ExactNumberQ[#]&)]" args = args.get_sequence() @@ -246,7 +264,7 @@ def eval_other(self, args, evaluation): c = self.equal2(x, y, max_extra_prec) if c is None: return - if not self._op(c): + if not self.operator_sense(c): return SymbolFalse return SymbolTrue @@ -256,7 +274,15 @@ class _MinMax(Builtin): A_FLAT | A_NUMERIC_FUNCTION | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED ) - def eval(self, items, evaluation): + # "sense" should be either 1 for Maximum or -1 for Minimum + # This field is used to in comparison if figure out + # maximum/minimum sense. + # Below we default to value used for the Max builtin function + # The Min builtin function will redefine this. + + sense = 1 + + def eval(self, items, evaluation: Evaluation): "%(name)s[items___]" if hasattr(items, "flatten_with_respect_to_head"): items = items.flatten_with_respect_to_head(SymbolList) @@ -313,6 +339,51 @@ def pairs(elements): return to_sympy(expr, **kwargs) +class Between(Builtin): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/Between.html + +
+
'Between'[$x$, {$min$, $max$}] +
equivalent to $min$ <= $x$ <= $max$. +
'Between[$x$, { {$min1$, $max1$}, {$min2$, $max2$}, ...]' +
equivalent to $min1$ <= $x$ <= $max1$' || $min2$ <= $x$ <= $max2$ ... +
'Between[$range$]' +
operator form that yields 'Between'[$x$, $range$] when applied to expression $x$. +
+ + Check that 6 is in range 4..10: + >> Between[6, {4, 10}] + = True + + Same as above in operator form: + >> Between[{4, 10}][6] + = True + + 'Between' works with irrational numbers: + >> Between[2, {E, Pi}] + = False + + If more than an interval is given, 'Between' returns 'True' if $x$ belongs \\ + to one of them: + + >> {Between[3, {1, 2}, {4, 6}], Between[5, {1, 2}, {4, 6}]} + = {False, True} + """ + + attributes = A_PROTECTED + + rules = { + "Between[x_, {min_, max_}]": "min <= x <= max", # FIXME add error checking + "Between[x_, ranges__List]": "Module[{range},Do[If[Between[x,range], Return[True]],{range, {ranges}}]===True]", + "Between[range_List][x_]": "Between[x, range]", # operator form + } + + summary_text = "test if value or values are in range" + + class BooleanQ(Builtin): """ @@ -477,8 +548,8 @@ def get_pairs(args): yield (args[i], args[i + 1]) @staticmethod - def _op(x): - return x + def operator_sense(value): + return value class Greater(_ComparisonOperator, _SympyComparison): @@ -555,10 +626,10 @@ class Inequality(Builtin): } summary_text = "chain of inequalities" - def eval(self, items, evaluation): - "Inequality[items___]" + def eval(self, elements, evaluation: Evaluation): + "Inequality[elements___]" - elements = numerify(items, evaluation).get_sequence() + elements = numerify(elements, evaluation).get_sequence() count = len(elements) if count == 1: return SymbolTrue @@ -657,8 +728,7 @@ class Max(_MinMax): = Max[2, a, b] """ - sense = 1 - summary_text = "the maximum value" + summary_text = "get maximum value" class Min(_MinMax): @@ -692,7 +762,7 @@ class Min(_MinMax): """ sense = -1 - summary_text = "the minimum value" + summary_text = "get minimum value" class SameQ(_ComparisonOperator): @@ -746,14 +816,14 @@ class SameQ(_ComparisonOperator): summary_text = "literal symbolic identity" - def eval_list(self, items, evaluation): - "%(name)s[items___]" - items_sequence = items.get_sequence() - if len(items_sequence) <= 1: + def eval_list(self, elements, evaluation: Evaluation): + "%(name)s[elements___]" + elements_sequence = elements.get_sequence() + if len(elements_sequence) <= 1: return SymbolTrue - first_item = items_sequence[0] - for item in items_sequence[1:]: + first_item = elements_sequence[0] + for item in elements_sequence[1:]: if not first_item.sameQ(item): return SymbolFalse return SymbolTrue @@ -840,7 +910,7 @@ class Unequal(_EqualityOperator, _SympyComparison): sympy_name = "Ne" @staticmethod - def _op(x): + def operator_sense(x): return not x @@ -880,14 +950,14 @@ class UnsameQ(_ComparisonOperator): summary_text = "not literal symbolic identity" - def eval_list(self, items, evaluation): - "%(name)s[items___]" - items_sequence = items.get_sequence() - if len(items_sequence) <= 1: + def eval_list(self, elements, evaluation: Evaluation): + "%(name)s[elements___]" + elements_sequence = elements.get_sequence() + if len(elements_sequence) <= 1: return SymbolTrue - for index, first_item in enumerate(items_sequence): - for second_item in items_sequence[index + 1 :]: + for index, first_item in enumerate(elements_sequence): + for second_item in elements_sequence[index + 1 :]: if first_item.sameQ(second_item): return SymbolFalse return SymbolTrue diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index 54c5a2b98..0408739e1 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -583,7 +583,7 @@ def from_sympy(self, elements: Tuple[BaseElement, ...]) -> Expression: # This has to come before MPMathFunction class SympyFunction(SympyObject): - def eval(self, z, evaluation: Evaluation): + def eval(self, elements, evaluation: Evaluation): # Note: we omit a docstring here, so as not to confuse # function signature collector ``contribute``. @@ -591,8 +591,8 @@ def eval(self, z, evaluation: Evaluation): # to call the corresponding sympy function. Arguments are # converted to python and the result is converted from sympy # - # "%(name)s[z__]" - return eval_sympy(self, z, evaluation) + # "%(name)s[elements]" + return eval_sympy(self, elements, evaluation) def get_constant(self, precision, evaluation, have_mpmath=False): try: diff --git a/test/test_main_preeval.py b/test/test_main_preeval.py index f558c3664..04119a3e8 100644 --- a/test/test_main_preeval.py +++ b/test/test_main_preeval.py @@ -29,7 +29,7 @@ def pre_evaluation_hook(query: BaseElement, evaluation: Evaluation): def mock_read_line(self, prompt=""): """ Replacement for reading a line of input from stdin. - Note that the particular expression is tied into the + Note that the particular expression is tied into the assert test we make in pre_evaluation_hook. """ return "1+2\n" @@ -37,7 +37,7 @@ def mock_read_line(self, prompt=""): # Set up a pre-evaluation hook. mathics_core.PRE_EVALUATION_HOOK = pre_evaluation_hook - definitions = Definitions(add_builtin=False, extension_modules=[]) + definitions = Definitions(add_builtin=False, extension_modules=tuple()) # Mock patch in our own terminal shell, but with a replace read_line() # routine we can use for testing.