Skip to content

Make calculator % agree with // - #607

Open
YunJ-Chang wants to merge 1 commit into
sysprog21:mainfrom
YunJ-Chang:calculator-floored-remainder
Open

YunJ-Chang wants to merge 1 commit into
sysprog21:mainfrom
YunJ-Chang:calculator-floored-remainder

Conversation

@YunJ-Chang

@YunJ-Chang YunJ-Chang commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem and behavior

The task calculator floors // but used JavaScript's truncating %, so (a // b) * b + a % b did not give back a when the operands had opposite signs. For example, -7 % 3 showed -1 while -7 // 3 showed -3.

This takes approach 1, accepted in #602: % now returns the remainder of floored division, so its result takes the sign of the divisor. For integers this matches Python. -7 % 3 is 2 and 7 % -3 is -2. // does not change.

Notes

The divisor is added only when the signs differ, because ((a % b) + b) % b would turn 0.1 % 1 into 0.10000000000000009. An exact multiple returns 0 rather than -0, because atan2 reads the sign. A non-finite divisor keeps the plain remainder, so -1 % 10^400 stays -1 where Python gives inf; this differs from Python on purpose, so a finite expression keeps working. Non-negative operands keep their results, and a zero divisor is still refused. docs/task-mode.md now states the behavior for negative operands and infinite divisors.

The identity is promised for integers while the arithmetic stays exact. // rounds the quotient before flooring it, so 1 // 0.1 is 10 where Python gives 9, and that case already failed before this change. This change adds cases of its own: (-9.3 // 3.1) * 3.1 + (-9.3 % 3.1) was -9.3 and is now -6.2, and (5.4 // -1.8) * -1.8 + (5.4 % -1.8) was 5.4 and is now 3.6. % now follows Python there, while -9.3 // 3.1 stays -3 where Python gives -4. Deriving // from the remainder, as Python does, would fix this, but it changes 1 // 0.1 from 10 to 9, which #602 ruled out, so // is unchanged here.

Testing

tests/browser/calculator.test.js adds negative operands, the identity across sign combinations, non-negative results, zero divisors, the atan2 case, and infinite divisors. The tests for negative operands, negative zero, and infinite divisors fail without the fix. ./scripts/test.sh passes; cargo-audit, cargo-deny, actionlint, desktop checks, and one venv-only Python test were skipped locally.

Close #602


Summary by cubic

Aligns the calculator's % with its floored //: % now returns the remainder of floored division, so the result takes the divisor's sign and (a // b) * b + a % b equals a for integers. -7 % 3 is now 2 instead of -1, matching Python, while // and non-negative operands are unchanged.

Notes

  • The identity is guaranteed for integers only; // rounds the quotient before flooring, so 1 // 0.1 stays 10 and fractional operands can drift.
  • An infinite divisor leaves the dividend unchanged, so -1 % 10^400 is -1 instead of Python's -inf.
  • Docs and browser tests updated to cover sign combinations, zero divisors, infinite divisors, and unchanged positive results.

Written for commit 5be0cfa. Summary will update on new commits.

View guided diff

cubic-dev-ai[bot]

This comment was marked as resolved.

jserv

This comment was marked as outdated.

@jserv jserv added the explained A maintainer has seen the contributor explain this large change label Nov 6, 2026
@YunJ-Chang
YunJ-Chang force-pushed the calculator-floored-remainder branch from b966b2f to 6469cbe Compare November 6, 2026 16:19
Comment thread web/calculator.js Resolved
jserv

This comment was marked as outdated.

The task calculator floors // but used the JavaScript remainder for %,
which truncates, so with operands of opposite signs -7 // 3 was -3 while
-7 % 3 was -1, and (a // b) * b + a % b no longer gave back a. Floor %
so that its result takes the divisor's sign, as Python does. An exact
multiple returns 0, since atan2 reads the sign of -0. An infinite
divisor keeps the plain remainder, so a finite result stays finite where
Python gives inf. Non-negative results and the refusal of a zero divisor
stay. Fractional operands can miss the identity because // rounds the
quotient first: 1 // 0.1 is 10, and this change adds cases such as (-9.3
// 3.1) * 3.1 + (-9.3 % 3.1), which was -9.3 and is now -6.2, because //
stays as it was.

Close sysprog21#602

Change-Id: Iab7c91465603f1c4fc4e111d9e89be837e59b8b3
@YunJ-Chang
YunJ-Chang force-pushed the calculator-floored-remainder branch from 6469cbe to 5be0cfa Compare November 7, 2026 03:56
@YunJ-Chang

Copy link
Copy Markdown
Contributor Author

Updated at the current head, rebased onto main: % returns 0 for an exact multiple, a non-finite divisor keeps the plain remainder, and the doc states both limits on the identity.

One decision is yours: this change makes (-9.3 // 3.1) * 3.1 + (-9.3 % 3.1) give -6.2 instead of -9.3, because // keeps its rounding. Fixing that means deriving // from the remainder, which changes 1 // 0.1 from 10 to 9. Should I keep // as it is here, or make that change in this pull request or a follow-up?

@YunJ-Chang
YunJ-Chang requested a review from jserv November 7, 2026 04:06
Comment thread web/calculator.js
const remainder = dividend % divisor;
if (remainder === 0) return 0;
if (!Number.isFinite(divisor)) return remainder;
return remainder < 0 !== divisor < 0 ? remainder + divisor : remainder;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A tiny negative dividend with a positive divisor makes remainder + divisor round to exactly divisor: -1e-20 % 3 is 3, and 1e-20 % -3 is -3. That breaks the range promise that the result lies strictly between 0 and the divisor, and (-1e-20 // 3) * 3 + (-1e-20 % 3) is 0 rather than -1e-20. Python gives 3.0 here too, so matching it is defensible, but the doc comment and docs/task-mode.md say the result takes the divisor's sign without mentioning this edge. State it next to the fractional-operand caveat, or return 0 when the sum equals divisor if you prefer to keep the range strict.

assert.equal(evaluate(text), value, text);
});

test("% leaves a non-negative dividend and divisor alone", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing pins the rounding edge in flooredRemainder. Add ["-1e-20 % 3", 3] and ["1e-20 % -3", -3] here, or to a separate test, so whichever behavior you pick (document it or clamp to 0) is locked in and a later "fix" cannot change it unnoticed.

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase the current branch onto the upstream default branch and resolve any conflicts. Run /codetrial-review in AI agents (use $codetrial-review in Codex) and resolve all identified issues. Rework the series into minimal, logically coherent commits, squash closely related commits, and ensure all commit messages follow the project’s rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

explained A maintainer has seen the contributor explain this large change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calculator % disagrees with // on negative operands

2 participants