Skip to content

Give the three numeric types one remainder between them (#708) - #709

Merged
Rafael-SOWNet merged 1 commit into
ASC-Community:masterfrom
Rafael-SOWNet:fix/numeric-modulus-consistency
Aug 4, 2026
Merged

Rafael-SOWNet merged 1 commit into
ASC-Community:masterfrom
Rafael-SOWNet:fix/numeric-modulus-consistency

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

Fixes #708.

The three numeric types answered % three different ways, and two of the answers were wrong. Because Integer and Rational are both Real, which one applied depended on the static type at the call site rather than on the values.

7 % 3 -7 % 3 7 % -3 -7 % -3
Integer before 1 2 throws throws
Real before 1 -1 1 -1
Rational before (7/2 % 3 etc.) 1/2 5/2 1/2 -7/2
all three, after 1 2 -2 -1
  • Integer used EInteger.Mod, which refuses a negative divisor, so Integer.Create(7) % Integer.Create(-3) threw ArithmeticException: Divisor is negative — a public operator throwing on ordinary input.
  • Real truncated, taking the sign of the dividend where the other two took the sign of the divisor.
  • Rational added the divisor whenever the truncated remainder came out negative. That is the correct truncated-to-floored conversion only when the divisor is positive; for a negative one it moves the answer further from zero. (-7/2) % (-3) came back as -7/2 — larger in magnitude than the divisor, so not a remainder under any convention. The code carried a // TODO: consider the case for the divisor to be negative saying exactly this.

Why floored

All three now compute a - b*floor(a/b), so the remainder takes the sign of the divisor. This is what SymPy, Mathematica and Maxima answer, and the convention under which the residues modulo n are the numbers 0 to n-1.

Checked against SymPy 1.14 rather than asserted, integer and rational alike:

Mod(7/2, 3) = 1/2      Mod(-7/2, 3) = 5/2
Mod(7/2, -3) = -5/2    Mod(-7/2, -3) = -1/2

It is also what the library already does by hand where it needs a remainder it can rely on — ExpressionNumerical.Equality.cs carries a private TrueRemainder that converts C#'s truncation into precisely this.

What actually changes for callers

For Integer and Rational, only the broken cases move: the two that threw, and the one that was wrong.

For Real this is a behaviour change — -7 % 3 was -1 and is now 2. That is the point of the PR rather than a side effect, and it is why #708 is an issue rather than a quiet fix. Worth a maintainer's eye.

Tests

27 new, of which 12 fail without the change. Two are property tests rather than tables, since between them they are the definition of a remainder:

  • it is strictly smaller in magnitude than the divisor — which is what the Rational case was failing;
  • a - (a % b) is a whole multiple of b.

Suite: Failed: 0, Passed: 4572, Skipped: 14, Total: 4586, with no internal call site disturbed — the places that use % internally all divide by a positive constant.

Independent of #703, which needs a remainder and computes the floored one itself rather than going through these operators. If both land, #703 could be simplified to use % directly; I have left it alone so the two can be reviewed separately.

They answered three different ways, and two of the answers were wrong:

  Integer threw ArithmeticException on a negative divisor, because
  EInteger.Mod refuses one. A public operator, on ordinary input.

  Real truncated, so it took the sign of the dividend where the other two took
  the sign of the divisor.

  Rational added the divisor whenever the truncated remainder came out
  negative, which is the right conversion only for a positive divisor. For a
  negative one it moved the answer away from zero, so (-7/2) % (-3) came back
  as -7/2 -- larger in magnitude than the divisor, and a remainder under no
  convention at all. The code carried a TODO saying as much.

Integer and Rational are both Real, so which of the three applied depended on
the static type at the call site rather than on the values.

All three are now floored: the remainder takes the sign of the divisor, and
a - b*floor(a/b) is what every one of them computes. Checked against SymPy 1.14
on all four sign pairs, integer and rational alike. It is also what this library
already did by hand where it needed a remainder it could rely on --
ExpressionNumerical.Equality carries a private TrueRemainder that converts C#'s
truncation into exactly this.

For Integer and Rational only the broken cases move. Real is a behaviour change:
-7 % 3 was -1 and is 2.

12 of the 27 new tests fail without it, two of them property tests -- that the
remainder is smaller than the divisor, and that the difference is a whole
multiple of it, which between them are the definition. Suite 4572 passed,
0 failed, with no internal call site disturbed.
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.83%. Comparing base (90c00a8) to head (eb91ede).
⚠️ Report is 82 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #709      +/-   ##
==========================================
+ Coverage   80.99%   81.83%   +0.83%     
==========================================
  Files         155      159       +4     
  Lines       13687    13807     +120     
  Branches     1957     2333     +376     
==========================================
+ Hits        11086    11299     +213     
+ Misses       1990     1854     -136     
- Partials      611      654      +43     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Rafael-SOWNet
Rafael-SOWNet merged commit da6e5b8 into ASC-Community:master Aug 4, 2026
26 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/numeric-modulus-consistency branch August 4, 2026 23:46
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.

The numeric % operators disagree with each other, and two of them are wrong

1 participant