diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index a748a6c98..d36c7be59 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -440,6 +440,7 @@ System`Exponent System`Export System`ExportString System`Expression +System`ExtendedGCD System`Extract System`FaceForm System`Factor diff --git a/mathics/builtin/intfns/divlike.py b/mathics/builtin/intfns/divlike.py index 1817e4fa9..0e4d70773 100644 --- a/mathics/builtin/intfns/divlike.py +++ b/mathics/builtin/intfns/divlike.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - """ Division-Related Functions """ import sys -from typing import Callable, Optional, Union +from typing import Final from sympy import Q, ask @@ -33,6 +31,8 @@ SymbolQuotientRemainder, ) from mathics.eval.intfns.divlike import ( + combine_integer_arguments, + eval_ExtendedGCD, eval_GCD, eval_LCM, eval_ModularInverse, @@ -98,44 +98,82 @@ class Divisible(Builtin): attributes = A_LISTABLE | A_PROTECTED | A_READ_PROTECTED eval_error = Builtin.generic_argument_error expected_args = range(2, sys.maxsize) - rules = { + rules: Final[dict] = { "Divisible[n_, m_]": "Mod[n, m] == 0", } summary_text = "test whether one number is divisible by the other" -def combine_integer_arguments(builtin, ns, eval_fn: Callable) -> Optional[Expression]: +class ExtendedGCD(Builtin): """ - Build the result of GCD or LCM from its arguments ns: the integer - arguments are combined by eval_fn into a single Integer. If every - argument is an integer, that Integer is returned; otherwise a new - GCD[...] or LCM[...] expression is returned that holds the combined - integer followed by the remaining arguments. None, meaning no change, - is returned when there are fewer than two integers to combine. - - The rule pattern is ns___ (ns__) and not ns___Integer: the functions are - Flat, and ns___Integer also matches a lone integer among other - arguments. Rewriting it to itself made GCD[1/2, 3] or GCD[2.5, 5] - evaluate until $IterationLimit. + :Extended Euclidean algorithm: + https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm ( + :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcdex, + :WMA: + https://reference.wolfram.com/language/ref/ExtendedGCD.html) + +
+
'ExtendedGCD'[$n_1$, $n_2$, ...] +
computes the extended greatest common divisor of integers $n_1$, $n_2$, ... +
+ + Get the GCD and coefficients for the GCD of 2 and 3: + >> {gcd, {a, b}} = ExtendedGCD[2, 3] + = {1, {-1, 1}} + + Now reconstruct the GCD using just the coefficients and original numbers and check it matches 'gcd': + >> 2 a + 3 b == gcd + = True + + Get the GCD and coefficients for the GCD of 6, 15, and 30: + >> {gcd, {a, b, c}} = ExtendedGCD[6, 15, 30] + = {3, {-2, 1, 0}} + + Again, reconstruct the gcd using just the coefficients and original numbers and check it matches 'gcd': + >> 6 a + 15 b + 30 c == gcd + = True + + ExtendedGCD[3, {5, 15}] theads element-wise over lists: + >> ExtendedGCD[3, {5, 15}] + = {{1, {2, -1}}, {3, {1, 0}}} + + When one integer argument is given, the answer degenerates to a GCD of the number with coefficient 1: + >> ExtendedGCD[5] + = {5, {1}} + + And for a single negative integer, the coefficient is -1: + >> ExtendedGCD[-5] + = {5, {-1}} + + The Extended GCD of 0 gives coefficient 0: + >> ExtendedGCD[0] + = {0, {0}} + + See also + :'GCD': + /doc/reference-of-built-in-symbols/integer-functions/division-related-functions/gcd/. """ - ns = ns.get_sequence() - integers = [n for n in ns if isinstance(n, Integer)] - if len(integers) == len(ns): - return eval_fn(ns) - if len(integers) < 2: - return None - combined = eval_fn(integers) - if combined is None: - return None - others = [n for n in ns if not isinstance(n, Integer)] - return Expression(Symbol(builtin.get_name()), combined, *others) + + attributes = A_LISTABLE | A_PROTECTED + eval_error = Builtin.generic_argument_error + expected_args = range(1, sys.maxsize) + sympy_name = "gcdex" + summary_text = "compute the extended Greatest Common Divisor" + + def eval(self, ns, evaluation: Evaluation) -> Expression | None: + "ExtendedGCD[ns__]" + if not isinstance(ns, (Integer, Expression)): + return + return eval_ExtendedGCD(ns) class GCD(Builtin): """ - - :WMA link: - https://reference.wolfram.com/language/ref/GCD.html + :Greatest common divisor: + https://en.wikipedia.org/wiki/Greatest_common_divisor ( + :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.gcd, + :WMA: + https://reference.wolfram.com/language/ref/GCD.html)
'GCD'[$n_1$, $n_2$, ...] @@ -152,13 +190,17 @@ class GCD(Builtin): = {2, 1, 4, 1, 2} 'GCD' does not work for rational numbers and Gaussian integers yet. + + See also + :'ExtendedGCD': + /doc/reference-of-built-in-symbols/integer-functions/division-related-functions/extendedgcd/. """ attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED summary_text = "compute the Greatest Common Divisor" sympy_name = "gcd" - def eval(self, ns, evaluation: Evaluation) -> Optional[Expression]: + def eval(self, ns, evaluation: Evaluation) -> Expression | None: "GCD[ns___]" return combine_integer_arguments(self, ns, eval_GCD) @@ -166,7 +208,10 @@ def eval(self, ns, evaluation: Evaluation) -> Optional[Expression]: class LCM(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/LCM.html + :Least common multiple: + https://en.wikipedia.org/wiki/Least_common_multiple ( + :SymPy:https://docs.sympy.org/latest/modules/polys/domainsref.html#sympy.polys.domains.IntegerRing.lcm, + :WMA link:https://reference.wolfram.com/language/ref/LCM.html)
'LCM'[$n_1$, $n_2$, ...] @@ -182,13 +227,13 @@ class LCM(Builtin): attributes = A_FLAT | A_LISTABLE | A_ONE_IDENTITY | A_ORDERLESS | A_PROTECTED eval_error = Builtin.generic_argument_error expected_args = range(1, sys.maxsize) - messages = { + messages: Final[dict[str, str]] = { "argm": "LCM called with 0 arguments; 1 or more arguments are expected.", } summary_text = "compute the Least Common Multiple" sympy_name = "lcm" - def eval(self, ns, evaluation: Evaluation) -> Optional[Expression]: + def eval(self, ns, evaluation: Evaluation) -> Expression | None: "LCM[ns__]" # LCM[] is answered by generic_argument_error. @@ -297,7 +342,7 @@ class ModularInverse(SympyFunction): summary_text = "get the modular inverse $k^(-1)$ mod $n$" sympy_name = "mod_inverse" - def eval(self, k: Integer, n: Integer, evaluation: Evaluation) -> Optional[Integer]: + def eval(self, k: Integer, n: Integer, evaluation: Evaluation) -> Integer | None: "ModularInverse[k_Integer, n_Integer]" return eval_ModularInverse(k.value, n.value) @@ -305,11 +350,11 @@ def eval(self, k: Integer, n: Integer, evaluation: Evaluation) -> Optional[Integ class PowerMod(Builtin): """ :Modular exponentiation: - https://en.wikipedia.org/wiki/Modular_exponentiation SymPy: :mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, - :nth_root_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.nth_root, - :sqrt_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.sqrt_mod, and - :Pow:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.Pow; :WMA link: - https://reference.wolfram.com/language/ref/PowerMod.html. + https://en.wikipedia.org/wiki/Modular_exponentiation (:SymPy mod_inverse:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.mod_inverse, + :SymPy nth_root_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.nth_root, + :SymPy sqrt_mod:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.sqrt_mod, + :SymPy Pow:https://docs.sympy.org/latest/modules/core.html#sympy.core.numbers.Pow; :WMA: + https://reference.wolfram.com/language/ref/PowerMod.html).
'PowerMod'[$x$, $y$, $m$] @@ -385,17 +430,17 @@ class PowerMod(Builtin): eval_error = Builtin.generic_argument_error expected_args = 3 - messages = { + messages: Final[dict[str, str]] = { "ninv": "`1` is not invertible modulo `2`.", } - rules = { + rules: Final[dict[str, str]] = { "PowerMod[a, l_List, m_?NumberQ]": "Map[PowerMod[a, #, m] &, l]", } summary_text = "compute modular exponentiation" - sympy_names = { + sympy_names: Final[dict[str, str]] = { -1: "mod_inverse", # b = -1 0: "nth_root_mod", # 0 < b < 1 and is an inverse 0.5: "sqrt_mod", # b = 1/2 @@ -458,7 +503,7 @@ class Quotient(Builtin): eval_error = Builtin.generic_argument_error expected_args = (2, 3) - rules = { + rules: Final[dict[str, str]] = { "Quotient[m_Complex, n_?NumberQ]": "Round[m / n]", "Quotient[m_?NumberQ, n_Complex]": "Round[m / n]", "Quotient[l_List, n_?NumberQ]": "Map[Quotient[#, m] &, l]", @@ -467,7 +512,7 @@ class Quotient(Builtin): summary_text = "compute integer quotient" - def eval(self, m, n, evaluation: Evaluation) -> Union[Symbol, Integer]: + def eval(self, m, n, evaluation: Evaluation) -> Symbol | Integer: "Quotient[m_?NumberQ, n_?NumberQ]" py_m = m.value @@ -483,9 +528,7 @@ def eval(self, m, n, evaluation: Evaluation) -> Union[Symbol, Integer]: return result return eval_Quotient(py_m, py_n, 0) - def eval_with_offset( - self, m, n, d, evaluation: Evaluation - ) -> Union[Symbol, Integer]: + def eval_with_offset(self, m, n, d, evaluation: Evaluation) -> Symbol | Integer: "Quotient[m_?NumberQ, n_?NumberQ, d_?NumberQ]" py_m = m.value py_n = n.value @@ -504,7 +547,8 @@ def eval_with_offset( class QuotientRemainder(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/QuotientRemainder.html + :Euclidian Division:https://en.wikipedia.org/wiki/Euclidean_division ( + :WMA link:https://reference.wolfram.com/language/ref/QuotientRemainder.html)
'QuotientRemainder[m, n]' diff --git a/mathics/core/atoms/__init__.py b/mathics/core/atoms/__init__.py index 2b2354d7d..de5ba1c49 100644 --- a/mathics/core/atoms/__init__.py +++ b/mathics/core/atoms/__init__.py @@ -36,23 +36,22 @@ from mathics.core.atoms.strings import String, StringFromPython __all__: Final[list[str]] = [ - "ByteArray", "COMPARE_PREC", - "Complex", "FP_MANTISA_BINARY_DIGITS", "MATHICS3_COMPLEX_I", "MATHICS3_COMPLEX_I_NEG", "NUMERICAL_CONSTANTS", + "ByteArray", + "Complex", "Integer", "Integer0", "Integer1", "Integer2", "Integer3", "Integer4", - "Integer310", "Integer10", + "Integer310", "IntegerM1", - "Integer1", "MachineReal", "MachineReal0", "MachineReal1", diff --git a/mathics/core/expression.py b/mathics/core/expression.py index 634b0e7a3..91eb1684f 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -1,11 +1,11 @@ # cython: language_level=3 -# -*- coding: utf-8 -*- import math from bisect import bisect_left +from collections.abc import Callable, Iterable, Sequence from itertools import chain from types import MethodType -from typing import Any, Callable, Iterable, Optional, Sequence, Union +from typing import Any, Optional import sympy from mathics_scanner.location import SourceRange, SourceRange2 @@ -150,7 +150,7 @@ def next_elem(): if len_elements != len(current[1]._elements): return False parents.append(current) - current = tuple((c._head for c in current)) + current = tuple(c._head for c in current) pos.append(0) else: # Atom is not the same than an expression return False @@ -269,7 +269,7 @@ def sliced(self, lower, upper): b = bisect_left(seq, upper) # all(val >= j for val in seq[b:]) new_sequences = tuple(x - lower for x in seq[a:b]) elif seq is not None: - new_sequences = tuple() + new_sequences = () else: new_sequences = None @@ -368,20 +368,20 @@ class Expression(BaseElement, NumericOperators, EvalMixin): _head: BaseElement _elements: tuple[BaseElement, ...] _sequences: Any - _cache: Optional[ExpressionCache] + _cache: ExpressionCache | None # (head, elements, element_order) of the last element_order built. - _element_order_cache: Optional[tuple[BaseElement, tuple[BaseElement, ...], tuple]] - elements_properties: Optional[ElementsProperties] - options: Optional[dict[str, Any]] + _element_order_cache: tuple[BaseElement, tuple[BaseElement, ...], tuple] | None + elements_properties: ElementsProperties | None + options: dict[str, Any] | None pattern_sequence: bool - location: Optional[Union[SourceRange, SourceRange2, MethodType]] + location: SourceRange | SourceRange2 | MethodType | None def __init__( self, head: BaseElement, *elements: BaseElement, - elements_properties: Optional[ElementsProperties] = None, - literal_values: Optional[tuple] = None, + elements_properties: ElementsProperties | None = None, + literal_values: tuple | None = None, ): self.options = None self.pattern_sequence = False @@ -403,7 +403,7 @@ def __init__( self.location = None # self.copy creates this - self.original: Optional[Expression] = None + self.original: Expression | None = None def __getnewargs__(self): return (self._head, self._elements) @@ -412,16 +412,12 @@ def __hash__(self): return hash(("Expression", self._head) + tuple(self._elements)) def __repr__(self) -> str: - return "" % ( - repr(self.head), - ", ".join([repr(element) for element in self.elements]), - ) + elements = ", ".join(repr(element) for element in self.elements) + return f"" def __str__(self) -> str: - return "%s[%s]" % ( - str(self.head), - ", ".join([str(element) for element in self.elements]), - ) + elements = ", ".join(str(element) for element in self.elements) + return f"{self.head}[{elements}]" def _as_sympy_function(self, **kwargs): function_name = sympy_name(self.head) @@ -539,24 +535,19 @@ def _does_not_contain_symbol(self, symbol_name: str) -> bool: return False symbols = cache.symbols - if symbols is not None and symbol_name not in symbols: - return True - else: - return False + return bool(symbols is not None and symbol_name not in symbols) def _rebuild_cache(self): cache = self._cache if cache is None: time = None - elif cache.symbols is None: - time = cache.time - elif cache.sequences is None: + elif cache.symbols is None or cache.sequences is None: time = cache.time else: return cache - sym = set((self.get_head_name(),)) + sym = {self.get_head_name()} seq = [] for i, element in enumerate(self._elements): @@ -625,7 +616,7 @@ def elements(self, values: Iterable): self._cache = None self._element_order_cache = None - def equal2(self, rhs: Any) -> Optional[bool]: + def equal2(self, rhs: Any) -> bool | None: """Mathics3 two-argument Equal (==) returns True if self and rhs are identical. """ @@ -660,7 +651,7 @@ def equal2(self, rhs: Any) -> Optional[bool]: def evaluate( self, evaluation: Evaluation, - ) -> Optional[BaseElement]: + ) -> BaseElement | None: """ Apply transformation rules and expression evaluation to ``evaluation`` via ``rewrite_apply_eval_step()`` until that method tells us to stop, @@ -671,7 +662,7 @@ def evaluate( if evaluation.timeout: return None - expr: Optional[BaseElement] = self + expr: BaseElement | None = self reevaluate = True limit = None iteration = 1 @@ -733,7 +724,7 @@ def evaluate( evaluation.recursion_depth = recursion_depth # And one more for the finally below evaluation.inc_recursion_depth() - raise ret + raise finally: # Restores the state evaluation.options = old_options @@ -775,7 +766,7 @@ def evaluate_elements(self, evaluation) -> "Expression": # head = head.evaluate_elements(evaluation) return Expression(head, *elements) - def filter(self, head, cond, evaluation: Evaluation, count: Optional[int] = None): + def filter(self, head, cond, evaluation: Evaluation, count: int | None = None): # faster equivalent to: Expression(head, [element in self.elements if cond(element)]) return structure(head, self, evaluation).filter(self, cond, count) @@ -949,7 +940,7 @@ def get_mutable_elements(self) -> list: def get_option_values( self, evaluation: Evaluation, allow_symbols=False, stop_on_error=True - ) -> Optional[dict]: + ) -> dict | None: """ Build a dictionary of options from an expression. For example, Symbol("Integrate").get_option_values(evaluation, allow_symbols=True) @@ -959,7 +950,7 @@ def get_option_values( values = self.flatten_with_respect_to_head(SymbolList).elements else: values = [self] - option_values: dict[str, Union[str, BaseElement]] = {} + option_values: dict[str, str | BaseElement] = {} for option in values: symbol_name = option.get_name() if allow_symbols and symbol_name: @@ -988,7 +979,7 @@ def get_option_values( # test/builtin/atomic/test_assignment.py and # ensuring we never use Expression(SymbolList, ...) # for ListExpression. - def get_rules_list(self) -> Optional[list]: + def get_rules_list(self) -> list | None: """ If the expression is of the form {pat1->expr1,... {pat_2,expr2},...} return a (python) list of rules. @@ -1055,6 +1046,7 @@ def _build_element_order(self) -> tuple: in Monomial's __cmp() for the reverse ordering when comparing symbols in the monomial. """ + exps: dict head = self._head numeric = is_numeric(self) @@ -1228,7 +1220,7 @@ def is_uncertain_final_definitions(self, definitions) -> bool: def has_form( self, heads: Iterable[Symbol] | Symbol, - *element_counts: Optional[int], + *element_counts: int | None, ) -> bool: """ element_counts: @@ -1367,13 +1359,12 @@ def eval_range(indices): element = elements[index] if not ( element.is_literal or element.has_form(SymbolUnevaluated, 1) - ): - if isinstance(element, EvalMixin): - new_value = element.evaluate(evaluation) - # We need id() because != by itself is too permissive - if id(element) != id(new_value): - recompute_properties = True - elements[index] = new_value + ) and isinstance(element, EvalMixin): + new_value = element.evaluate(evaluation) + # We need id() because != by itself is too permissive + if id(element) != id(new_value): + recompute_properties = True + elements[index] = new_value # @timeit def rest_range(indices): @@ -1383,24 +1374,25 @@ def rest_range(indices): return for index in indices: element = elements[index] - if not element.is_literal and element.has_form( - SymbolEvaluate, 1 + if ( + not element.is_literal + and element.has_form(SymbolEvaluate, 1) + and isinstance(element, EvalMixin) ): - if isinstance(element, EvalMixin): - new_value = element.evaluate(evaluation) - # We need id() because != by itself is too permissive - if id(new_value) != id(element): - elements[index] = new_value - recompute_properties = True + new_value = element.evaluate(evaluation) + # We need id() because != by itself is too permissive + if id(new_value) != id(element): + elements[index] = new_value + recompute_properties = True if (A_HOLD_ALL | A_HOLD_ALL_COMPLETE) & attributes: # eval_range(range(0, 0)) rest_range(range(len(elements))) elif A_HOLD_FIRST & attributes: - rest_range(range(0, min(1, len(elements)))) + rest_range(range(min(1, len(elements)))) eval_range(range(1, len(elements))) elif A_HOLD_REST & attributes: - eval_range(range(0, min(1, len(elements)))) + eval_range(range(min(1, len(elements)))) rest_range(range(1, len(elements))) else: eval_range(range(len(elements))) @@ -1686,14 +1678,14 @@ def rules(): def round_to_float( self, evaluation=None, permit_complex=False - ) -> Optional[Union[float, complex]]: + ) -> float | complex | None: """ Round to a Python float. Return None if rounding is not possible. This can happen if self or evaluation is NaN. """ if evaluation is None: - value: Optional[BaseElement] = self + value: BaseElement | None = self elif isinstance(evaluation, sympy.core.numbers.NaN): return None else: @@ -1794,7 +1786,7 @@ def to_python(self, *args, **kwargs) -> Any: # If self.value is legitimately None, we'll # catch it further down. if hasattr(self, "value") and self.value is not None: - if self.value == tuple() and kwargs.get("prefer_dictionary"): + if self.value == () and kwargs.get("prefer_dictionary"): return {} return self.value @@ -2237,7 +2229,7 @@ def construct(elements): # Note: this function is called a *lot* so it needs to be fast. def convert_expression_elements( elements: Iterable, conversion_fn: Callable = from_python, is_uniform: bool = True -) -> tuple[tuple, ElementsProperties, Optional[tuple]]: +) -> tuple[tuple, ElementsProperties, tuple | None]: """ Convert and return a tuple of Elements from the Python-like items in `elements`, along with elements properties of the elements tuple, diff --git a/mathics/eval/intfns/divlike.py b/mathics/eval/intfns/divlike.py index 637983724..1e6bbe5e2 100644 --- a/mathics/eval/intfns/divlike.py +++ b/mathics/eval/intfns/divlike.py @@ -2,28 +2,68 @@ Mathics3 builtins from mathics.builtin.intfns.divlike """ -from typing import Optional, Union +from collections.abc import Callable import sympy -import mathics.eval.tracing as tracing -from mathics.core.atoms import Complex, Integer, Integer0 +import mathics.eval.tracing as tracing # noqa +from mathics.core.atoms import Complex, Integer, Integer0, Integer1, IntegerM1 +from mathics.core.convert.expression import to_mathics_list from mathics.core.convert.python import from_python from mathics.core.convert.sympy import from_sympy from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Symbol -def get_unit_inverse(f: float) -> Optional[int]: +def combine_integer_arguments(builtin, ns, eval_fn: Callable) -> Expression | None: """ - If `f` is a unit inverse, that is, 1 / some_int, - then some_int. Otherwise return None + Build the result of GCD or LCM from its arguments ns: the integer + arguments are combined by eval_fn into a single Integer. If every + argument is an integer, that Integer is returned; otherwise a new + GCD[...] or LCM[...] expression is returned that holds the combined + integer followed by the remaining arguments. None, meaning no change, + is returned when there are fewer than two integers to combine. + + The rule pattern is ns___ (ns__) and not ns___Integer: the functions are + Flat, and ns___Integer also matches a lone integer among other + arguments. Rewriting it to itself made GCD[1/2, 3] or GCD[2.5, 5] + evaluate until $IterationLimit. """ - if f <= 0: + ns = ns.get_sequence() + integers = [n for n in ns if isinstance(n, Integer)] + if len(integers) == len(ns): + return eval_fn(ns) + if len(integers) < 2: return None - return int(inv) if (inv := 1.0 / f) else None + combined = eval_fn(integers) + if combined is None: + return None + others = [n for n in ns if not isinstance(n, Integer)] + return Expression(Symbol(builtin.get_name()), combined, *others) -def eval_GCD(ns: tuple) -> Optional[Integer]: +def eval_ExtendedGCD(arguments: Expression | Integer) -> ListExpression: + if isinstance(arguments, Integer): + n = arguments.value + return ListExpression( + Integer(abs(n)), ListExpression(Integer((n > 0) - (n < 0))) + ) + integers: list[int] = [] + for element in arguments.elements: + if isinstance(element, Integer): + integers.append(element.value) + else: + # Handle things other than Integers? + return None + coeffs, current_gcd = gcdex_list(integers) + return ListExpression( + Integer(current_gcd), to_mathics_list(*coeffs, elements_conversion_fn=Integer) + ) + + +def eval_GCD(ns: tuple) -> Integer | None: if len(ns) == 0: return Integer0 if (result := ns[0].value) is None: @@ -36,7 +76,7 @@ def eval_GCD(ns: tuple) -> Optional[Integer]: return Integer(abs(result)) -def eval_LCM(ns: tuple) -> Optional[Integer]: +def eval_LCM(ns: tuple) -> Integer | None: if len(ns) == 0: return Integer0 if (result := ns[0].value) is None: @@ -49,7 +89,7 @@ def eval_LCM(ns: tuple) -> Optional[Integer]: return Integer(abs(result)) -def eval_ModularInverse(k: int, n: int) -> Optional[Integer]: +def eval_ModularInverse(k: int, n: int) -> Integer | None: try: r = tracing.run_sympy(sympy.mod_inverse, k, n) except ValueError: @@ -57,9 +97,9 @@ def eval_ModularInverse(k: int, n: int) -> Optional[Integer]: return Integer(r) -def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Union[Integer, Complex]: +def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Integer | Complex: """ - All the variaous forms of handling module exponentiation. + All the various forms of handling modular exponentiation. SymPy offers mod_inverse, nth_root_mod, sqrt_mod, and core.power.Pow. Python's `pow` is good for large ints. """ @@ -108,7 +148,7 @@ def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Union[Integer, Complex]: elif all(isinstance(x, int) for x in (a, b, m)) and b >= 1: # Standard Modular Exponentiation (b >= 1) using sympy.pow: # Use Python's builtin function `pow`. - # this handles large int's better than SymPy's core.power.Pow: + # This handles large int's better than SymPy's core.power.Pow: return Integer(pow(a, b, m)) elif b > 1: # Standard Modular Exponentiation (b >= 1) using sympy.pow: @@ -119,3 +159,41 @@ def eval_PowerMod(a, b, m, evaluation: Evaluation) -> Union[Integer, Complex]: def eval_Quotient(m, n, d) -> Integer: return Integer((m - d) // n) + + +def gcdex_list(numbers: list[int]) -> tuple[list[int], int]: + """ + Return the extended GCD's coefficients and cumulative GCD for a list of numbers using + SymPy's gcdex. + If the list is empty, we return []. + + """ + if not numbers: + return [], 0 + if len(numbers) == 1: + # Degenerate case. + return [1], numbers[0] + + # Start with the first element... + coeffs = [1] + current_gcd = numbers[0] + + # and then chain through the remaining elements. + for num in numbers[1:]: + x, y, current_gcd = tracing.run_sympy(sympy.gcdex, current_gcd, num) + # Scale all previously found coefficients by x + coeffs = [c * x for c in coeffs] + # Append the new coefficient y. + coeffs.append(y) + + return coeffs, current_gcd + + +def get_unit_inverse(f: float) -> int | None: + """ + If `f` is a unit inverse, that is, 1 / some_int, + then some_int. Otherwise return None + """ + if f <= 0: + return None + return int(inv) if (inv := 1.0 / f) else None diff --git a/test/builtin/intfns/test_divlike.py b/test/builtin/intfns/test_divlike.py index 9fe651572..b5bf886a1 100644 --- a/test/builtin/intfns/test_divlike.py +++ b/test/builtin/intfns/test_divlike.py @@ -107,7 +107,7 @@ ], ) def test_divlike(str_expr, msgs, str_expected, fail_msg): - """ """ + """tests functions Quotient, LCM, GCD""" check_evaluation( str_expr, str_expected,