Skip to content

Commit 172e0af

Browse files
committed
fix(round): strategy-dependent result for infinite step
round(A, B) with finite A and infinite B is not always 0 — the multiples of an infinite step are {-∞, 0, +∞}, so up (ceiling) lands on +∞ for positive A and down (floor) lands on -∞ for negative A. nearest/to-zero still collapse to ±0 carrying A's sign. Previously the !isFinite(b) branch returned signed zero for every strategy, so round(up, 5, infinity) yielded 0 instead of calc(infinity) and round(down, -5, infinity) yielded 0 instead of calc(-infinity). Also fixes the dimensional form (round(up, 5px, calc(infinity * 1px))). Corrects the general.test.mjs case that had codified the buggy output and adds strategy-dependent coverage to round.test.mjs.
1 parent bc74d58 commit 172e0af

3 files changed

Lines changed: 49 additions & 17 deletions

File tree

src/lib/simplify/round.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,25 @@ function simplifyRound(args) {
4242
if (!fold) {return passthrough();}
4343

4444
const [a, b] = /** @type {[number, number]} */ (fold.values);
45-
// Spec §10.7.1 non-finite step: NaN propagates; infinite step with
46-
// finite A folds to ±0 carrying A's sign (every strategy picks the same
47-
// single point); both infinite cancels to NaN. Infinite-A / finite-B
48-
// falls through to applyRound, where floor*b===ceil*b===±∞ collapses
49-
// back to A (§10.3.1 "result is the same infinity").
45+
// Spec §10.7.1 non-finite step: NaN propagates; both infinite cancels to
46+
// NaN. Infinite step with finite A is strategy-dependent — the multiples
47+
// of an infinite step are {-∞, 0, +∞}, so `up` (ceiling) lands on +∞ for
48+
// positive A and `down` (floor) lands on -∞ for negative A; every other
49+
// case folds to ±0 carrying A's sign. Infinite-A / finite-B falls through
50+
// to applyRound, where floor*b===ceil*b===±∞ collapses back to A
51+
// (§10.3.1 "result is the same infinity").
5052
if (isNaN(b)) {return num(NaN);}
5153
if (!isFinite(b)) {
5254
if (!isFinite(a)) {return num(NaN);}
53-
const signedZero = a < 0 || Object.is(a, -0) ? -0 : 0;
54-
return fold.unit === '' ? num(signedZero) : dim(signedZero, fold.unit);
55+
let result;
56+
if (strategy === 'up' && a > 0) {
57+
result = Infinity;
58+
} else if (strategy === 'down' && a < 0) {
59+
result = -Infinity;
60+
} else {
61+
result = a < 0 || Object.is(a, -0) ? -0 : 0;
62+
}
63+
return fold.unit === '' ? num(result) : dim(result, fold.unit);
5564
}
5665

5766
const result = applyRound(strategy, a, b);

test/unit/simplify/general.test.mjs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,16 @@ test('spec §10.3.1 line 1020: round(A, 0) is NaN', () => {
221221
assert.equal(out('round(down, 5, 0)'), 'calc(NaN)');
222222
assert.equal(out('round(to-zero, 5, 0)'), 'calc(NaN)');
223223
});
224-
test("spec §10.7.1: round(finite, ±infinity) → 0 carrying A's sign", () => {
225-
// floor(A/±∞) = ceil(A/±∞) = ±0; the spec collapses that interval to
226-
// the nearest multiple of step, which is 0 with sign of A. Strategy
227-
// doesn't change the result. Found by the differential randomizer.
228-
assert.equal(out('round(5, infinity)'), '0');
229-
assert.equal(out('round(up, 5, infinity)'), '0');
230-
// The simplifier produces -0 for negative A here, but the serializer
231-
// collapses sign-of-zero to match CSS's numeric `-0 === 0` and the
232-
// existing test expectations across abs/sign/etc.
233-
assert.equal(out('round(down, calc(0 - 5), infinity)'), '0');
224+
test('spec §10.7.1: round(finite, ±infinity) is strategy-dependent', () => {
225+
// The multiples of an infinite step are {-∞, 0, +∞}. up (ceiling) lands
226+
// on +∞ for positive A; down (floor) lands on -∞ for negative A; nearest
227+
// and to-zero collapse to 0 carrying A's sign. The serializer collapses
228+
// sign-of-zero to match CSS's numeric `-0 === 0`.
229+
assert.equal(out('round(5, infinity)'), '0'); // nearest default
230+
assert.equal(out('round(up, 5, infinity)'), 'calc(infinity)');
231+
assert.equal(out('round(down, calc(0 - 5), infinity)'), 'calc(-infinity)');
232+
assert.equal(out('round(down, 5, infinity)'), '0');
233+
assert.equal(out('round(up, calc(0 - 5), infinity)'), '0');
234234
assert.equal(out('round(3, calc(0 - infinity))'), '0');
235235
});
236236
test('spec §10.7.1: round(_, NaN) is NaN', () => {

test/unit/simplify/round.test.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,29 @@ test('round: A infinite, B finite → same infinity (§10.3.1 line 1022)', () =>
9191
assert.equal(out('round(up, infinity, 10)'), 'calc(infinity)');
9292
assert.equal(out('round(down, calc(0 - infinity), 10)'), 'calc(-infinity)');
9393
});
94+
test('round: A finite, B infinite → strategy-dependent (§10.3.1)', () => {
95+
// Multiples of an infinite step are {-∞, 0, +∞}.
96+
// up (ceiling) lands on +∞ for positive A; down (floor) lands on -∞ for
97+
// negative A; every other case folds to ±0 carrying A's sign.
98+
assert.equal(out('round(up, 5, infinity)'), 'calc(infinity)');
99+
assert.equal(out('round(down, calc(0 - 5), infinity)'), 'calc(-infinity)');
100+
assert.equal(out('round(down, 5, infinity)'), '0');
101+
assert.equal(out('round(up, calc(0 - 5), infinity)'), '0');
102+
assert.equal(out('round(nearest, 5, infinity)'), '0');
103+
assert.equal(out('round(to-zero, 5, infinity)'), '0');
104+
// Dimensional A with an infinite dim step keeps the unit.
105+
assert.equal(
106+
out('round(up, 5px, calc(infinity * 1px))'),
107+
'calc(infinity * 1px)'
108+
);
109+
assert.equal(
110+
out('round(down, calc(0px - 5px), calc(infinity * 1px))'),
111+
'calc(-infinity * 1px)'
112+
);
113+
});
114+
test('round: A and B both infinite → NaN', () => {
115+
assert.equal(out('round(infinity, infinity)'), 'calc(NaN)');
116+
});
94117
test('round: zero A → 0 (exact multiple)', () => {
95118
assert.equal(out('round(0, 5)'), '0');
96119
assert.equal(out('round(up, 0, 5)'), '0');

0 commit comments

Comments
 (0)