Skip to content

Add round, min, max and gcd, closing #809 - #828

Merged
Rafael-SOWNet merged 3 commits into
masterfrom
feat/round-min-max-gcd
Aug 9, 2026
Merged

Add round, min, max and gcd, closing #809#828
Rafael-SOWNet merged 3 commits into
masterfrom
feat/round-min-max-gcd

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Collaborator

Closes #809. Follows #827, which added floor and ceil.

Every convention measured, and two were not what the issue assumed

round is half to even

round(1/2)  = 0     round(-1/2) = 0
round(3/2)  = 2     round(-3/2) = -2
round(5/2)  = 2     round(-5/2) = -2

Python, SymPy, Mathematica and IEEE 754 all mean this by rounding, and it is .NET's Math.Round default. It is deliberately not floor(x + 1/2) — the obvious translation, which disagrees at every tie: that sends 1/2 to 1 and 5/2 to 3. There is a test pinning that the two differ, so nobody simplifies the node into it later.

That also decided the SymPy export. SymPy has no symbolic roundRoundFunction is an abstract base that raises, and .round() is a method on a concrete number, not a function of an expression. So ToSymPy builds the tie correction out of floor, frac and Mod rather than emitting the wrong one-liner, and the construction was checked against SymPy's own Rational.round() on ties, non-ties and negatives.

gcd is not integers only

The issue said "only defined on integers" and I repeated it. SymPy's gcd is the polynomial gcd with the integer case inside it:

gcd(12, 18)      = 6
gcd(1/2, 1/3)    = 1/6        rationals, not just integers
gcd(x**2-1, x-1) = x - 1      polynomials

Integers and rationals are computed here. The polynomial case is left as the node rather than half-done — this library already has PolynomialGcd behind the PolynomialGcdCancellation rule set, and wiring that in is its own change.

min and max are nodes, not sugar

Expressible as (a + b ∓ |a − b|)/2, and SymPy still keeps Min/Max as nodes. That is the right call: the closed form is what they simplify to where it helps, not what they should print as — and the round-trip contract would make a sugar spelling permanent, since min(a, b) could then never print as min.

They compare only where the arguments are ordered; an unordered pair is left alone rather than guessed at. Their derivative stays unevaluated, because stating it needs a case split on which argument is smaller — declining is the honest answer there.

Variadic

min, max and gcd take any number of arguments and fold left, as they do everywhere else: min(3, 5, 1) is 1. That needed one new helper, AssertAtLeast, since every existing arity check is for a fixed count.

Tests

5928 passing, 0 failed, 14 skipped. F# 130 passing.

RoundMinMaxGcdTest covers the tie behaviour in both directions, that round differs from floor(x + 1/2), comparison and folding, gcd over integers and rationals, printing, LaTeX, round-trips, derivatives, arity errors and substitution.

Twelve tests in MissingFunctionNamesRefusedTest asserted these names were refused and no longer pass. Same category as last time and worth naming: that is the answer getting better, not a test pinning a fudge. The refusal cases move to trunc, lcm, erf and conjugate, which are what is still missing.

Compatibility

BREAKING-CHANGES.md records it. If you were catching UnrecognizedFunctionParseException around these names, that is dead code now. Bare min, max and floor without a bracket are still ordinary variables, and minimum(x) is still the implicit product.

🤖 Generated with Claude Code

Rafael-SOWNet and others added 3 commits August 9, 2026 00:01
Both were refused by name since #733. They exist now, as nodes rather than as
sugar, with every convention measured against SymPy 1.14 rather than chosen --
AGENTS.md asks for exactly that when a convention has to be settled.

What the measurement decided, and in three places it is not what the issue
assumed:

- Rounding is toward the infinities, not toward zero: floor(-3/2) is -2 and
  ceil(-3/2) is -1.
- A complex argument is taken componentwise, so floor(3/2 + 5/2i) is 1 + 2i.
  SymPy and Mathematica agree, and it is the only reading under which floor of a
  real keeps its meaning when the imaginary part is zero.
- The derivative is 0 provided the argument is not an integer. The issue guessed
  "zero almost everywhere"; SymPy declines to answer at all. Stating the condition
  says more than either, and it is what Signumf and Absf already do -- this
  library has Providedf and SymPy does not.

ceiling( is accepted because that is SymPy's spelling, so an expression copied
from there parses; Stringize prints the short ceil(, which is what the round trip
pins.

Inverting them is many-to-one: floor(f(x)) = v is solvable only for integer v and
then f(x) is anywhere in [v, v + 1). So the inverse is a parameter over that
interval, the same device Signumf and Absf use, and

  floor(x) - 3 = 0

now answers { 3 + t_1 provided t_1 in RR and t_1 >= 0 and t_1 < 1 } where #733
recorded it answering { 3 / floor }.

The grammar was regenerated with JDK 25 and the committed ANTLR 4.13.1 jar. The
unmodified grammar was regenerated first and reproduced the checked-in parser
byte for byte, so the diff here is the new rules and not a toolchain difference.

Seven tests in MissingFunctionNamesRefusedTest asserted these names were refused.
That is the answer getting better rather than a test that was pinning a fudge:
the refusal cases move to names still missing, and the equation from the original
report is now asserted to be answered instead of refused.

Tests: 5872 passing, 0 failed, 14 skipped; F# 130.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four that were left after floor and ceil. Every convention was measured
against SymPy 1.14 rather than chosen, and two of them are not what the issue
assumed.

round is half to even -- round(1/2) is 0, round(5/2) is 2 -- matching Python,
SymPy, Mathematica and IEEE 754, and .NET's Math.Round default. It is
deliberately not floor(x + 1/2), which is the obvious translation and disagrees
at every tie: that sends 1/2 to 1 and 5/2 to 3. A test pins that the two differ,
so nobody simplifies the node into it.

That also decided the SymPy export. SymPy has no symbolic round -- RoundFunction
is an abstract base that raises, and .round() is a method on a concrete number --
so ToSymPy builds the tie correction out of floor, frac and Mod rather than
emitting the wrong one-liner. Checked against Rational.round() on ties, non-ties
and negatives.

min and max are nodes, not sugar over (a + b -+ abs(a - b)) / 2. The closed form
is what they simplify to where it helps; it is not what they should print as, and
the round-trip contract would make that permanent. SymPy keeps Min as a node for
the same reason. They compare only where the arguments are ordered, so an
unordered pair is left alone rather than guessed at, and their derivative stays
unevaluated because stating it needs a case split on which argument is smaller.

gcd is not integers only, which is what the issue said and what I repeated. SymPy's
gcd is the polynomial gcd with the integer case inside it, so gcd(1/2, 1/3) is 1/6
and gcd(x^2-1, x-1) is x-1. Integers and rationals are computed here; the
polynomial case is left as the node rather than half-done, and this library already
has PolynomialGcd for whoever wires it in.

min, max and gcd take any number of arguments and fold, which needed one new
helper -- AssertAtLeast -- since every existing arity check is for a fixed count.

The grammar was regenerated with JDK 25 and the committed ANTLR 4.13.1 jar, after
confirming on the previous change that the unmodified grammar reproduces the
checked-in parser byte for byte.

Twelve tests in MissingFunctionNamesRefusedTest asserted these names were refused,
and they are not any more. That is the answer getting better: the refusal cases
move to trunc, lcm, erf and conjugate, which are what is still missing.

Tests: 5928 passing, 0 failed, 14 skipped; F# 130.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rafael-SOWNet
Rafael-SOWNet merged commit 489f12c into master Aug 9, 2026
25 checks passed
Rafael-SOWNet added a commit that referenced this pull request Aug 9, 2026
* Bring Syntax.md up to date with the functions that now exist

Syntax.md still listed floor, ceil, ceiling, round, min, max and gcd under
"Refused by name". Six of them have existed since #827 and #828, so the document
was telling a caller the opposite of what the parser does -- and AGENTS.md is
explicit that a stale one of these is worse than none. I changed the grammar twice
and did not update it either time.

The entries say what each does rather than only that it parses, since the
conventions are the part a caller cannot guess: rounding toward the infinities
rather than zero, round going to the nearest even on a tie and so not being
floor(x + 1/2), the componentwise reading of a complex argument, min and max
leaving an unordered pair alone, and gcd covering rationals.

Every claim in the new text was checked against a build rather than written from
memory, including that the four still-refused names still raise and that re and im
are still read as products.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say in Syntax.md too that LaTeX output is parsed elsewhere

The same false claim #823 corrected in AGENTS.md -- "nothing parses LaTeX" -- was
in Syntax.md as well, and I fixed only the one I had been pointed at.
CSharpMath.Evaluation reads LaTeX back into an Entity and states the contract in
its own source, so the sentence was wrong in both places.

The point worth keeping is the one the correction adds: Latexise is still free to
use \frac and the rest, but a change to what it emits can break a downstream
project and nothing here will catch it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet added a commit that referenced this pull request Aug 9, 2026
… (#836)

ComputeLimitDivideEtImpera defaulted to new Limitf(this, x, dist, side). That is
the cycle AGENTS.md names, with this exact expression as its example: the caller
evaluates the returned node to compare it, evaluating a Limitf computes the limit,
and computing arrives back at the default. It overflows the stack, which kills the
process rather than raising anything a caller can catch.

So a node crashed by inheriting the default -- by not being mentioned anywhere.
Seven did:

  limit(floor(x), x, 0)     stack overflow, process dies
  limit(ceil(x), x, 0)      stack overflow
  limit(round(x), x, 0)     stack overflow
  limit(min(x, 1), x, 0)    stack overflow
  limit(max(x, 1), x, 0)    stack overflow
  limit(gcd(x, 2), x, 0)    stack overflow
  limit(phi(x), x, 0)       stack overflow

The first six arrived with #827 and #828, which added the nodes without the
ComputeLimitDivideEtImpera override that AddingNode.cs lists at step 3d. phi
predates both and is #833, so this is not only my regression -- the default has
been a landmine for every node that ever inherited it, which is what #704 was.

Returning null is what AGENTS.md prescribes and what every caller already expects:
each of the eleven call sites tests the result with `is { }` or switches on it, and
the signature was already Entity?. All seven now come back as an unevaluated limit
node, which is the honest "I could not settle this", and nothing else moves --
5928 tests passed before this change and after it.

LimitTerminatesOnEveryNodeTest enumerates the node types by reflection rather than
listing them, because the defect is in what a node inherits by not being
mentioned, and a per-node test cannot catch that: 45 types, each over three
destinations and three sides, asserting only that the call returns. Checked
against the unfixed code, where the run aborts.

This does not replace #831, which gives floor and ceil real limit answers and
fixes the OverflowException of #830. That adds capability; this removes the way to
crash. They compose, and the order does not matter.

Tests: 5973 passing, 0 failed, 14 skipped; F# 130.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AngouriMath has no rounding functions (floor, ceil, round), nor min, max or gcd

1 participant