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,