Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/lib/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@
* @property {string} [calcName] Wrapper name to use when `calc()` is needed. Default `'calc'`.
*/

// Below this is float noise, not a value: `0.1 + 0.2 - 0.3` is 5.5e-17.
const NOISE_FLOOR = 1e-12;

/**
* Rounding to `prec` decimal places turns `calc(1/1000000)` into `0`, and a
* `0` in CSS is often a switch, not a small number (`flex-grow: 0` never
* grows). So when a value is too small for `prec`, keep its significant digits
* instead: `1/1000000` -> `0.000001`, `1/3000000` -> `3.3333e-7`.
*
* @param {number} v
* @param {number | false} prec
* @return {number}
Expand All @@ -25,7 +33,12 @@ function round(v, prec) {
return v;
}
const m = Math.pow(10, prec);
return Math.round(v * m) / m;
const rounded = Math.round(v * m) / m;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should be some comment about explaining why the code is doing this or nobody will understand it in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Added a comment on the noise floor and on round() itself

if (rounded === 0 && Math.abs(v) > NOISE_FLOOR) {
// toPrecision needs at least one significant digit; `prec` may be 0.
return Number(v.toPrecision(Math.max(prec, 1)));
}
return rounded;
}

// §10.13 / §10.7.2: Infinity/NaN serialize as canonical keywords.
Expand Down
1 change: 1 addition & 0 deletions src/lib/simplify/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { convert } = require('../convertUnits.js');
* @typedef {object} UnitBucket
* @property {string} unit
* @property {number} total
* @property {number} scale largest |term| accumulated into `total`, for noise detection
* @property {import('../convertUnits.js').BaseType | null} base
* @property {number} order
*/
Expand Down
21 changes: 19 additions & 2 deletions src/lib/simplify/sum.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ const { mergeConvertibleBuckets } = require('./bucket.js');
* @typedef {import('./bucket.js').UnitBucket} UnitBucket
*/

// Subtracting near-equal terms leaves float dust: `0.07 * 1e7 - 700000` is
// 1.16e-10, not 0. Snap a total that's tiny next to its terms back to 0.
const NOISE_REL = Number.EPSILON * 8;

/**
* @param {number} total
* @param {number} scale largest |term| accumulated into `total`
* @return {number}
*/
function denoise(total, scale) {
return Math.abs(total) < scale * NOISE_REL ? 0 : total;
}

/**
* @param {Sum} sum
* @param {SimplifyFn} simplify
Expand All @@ -23,6 +36,7 @@ function simplifySum(sum, simplify) {
// encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
// then vh/rem/px stay separate in phase 2 (none convert to each other).
let numTotal = 0;
let numScale = 0;
/** @type {Map<string, UnitBucket>} */
const byUnit = new Map();
/** @type {SumTerm[]} */
Expand All @@ -43,17 +57,20 @@ function simplifySum(sum, simplify) {
}
if (n.type === 'Num') {
numTotal += sign * n.value;
numScale = Math.max(numScale, Math.abs(n.value));
return;
}
if (n.type === 'Dim') {
const key = n.unit.toLowerCase();
const existing = byUnit.get(key);
if (existing) {
existing.total += sign * n.value;
existing.scale = Math.max(existing.scale, Math.abs(n.value));
} else {
byUnit.set(key, {
unit: n.unit,
total: sign * n.value,
scale: Math.abs(n.value),
base: baseOf(n.unit),
order: bucketOrder++,
});
Expand All @@ -71,9 +88,9 @@ function simplifySum(sum, simplify) {
// unconditionally is harmless. Zero-valued unit buckets are kept for
// type info (WPT calc-serialization-002).
/** @type {SumTerm[]} */
const terms = [{ sign: 1, node: num(numTotal) }];
const terms = [{ sign: 1, node: num(denoise(numTotal, numScale)) }];
for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
terms.push({ sign: 1, node: dim(bucket.total, bucket.unit) });
terms.push({ sign: 1, node: dim(denoise(bucket.total, bucket.scale), bucket.unit) });
}
terms.push(...opaque);

Expand Down
35 changes: 35 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,41 @@ test(
testValue('calc(5/1000000)', '0.000005', { precision: 6 })
);

test(
'should keep a value smaller than the precision instead of rounding it to zero',
testValue('calc(1/1000000)', '0.000001')
);

test(
'should keep a dimension smaller than the precision',
testValue('calc(1px/1000000)', '0.000001px')
);

test(
'should keep a negative value smaller than the precision',
testValue('calc(-1/1000000)', '-0.000001')
);

test(
'should keep the ratio between two values smaller than the precision',
testValue('calc(2/1000000)', '0.000002')
);

test(
'should limit a value smaller than the precision to that many significant digits',
testValue('calc(1/3000000)', '3.3333e-7')
);

test(
'should still round float noise down to zero',
testValue('calc(0.1px + 0.2px - 0.3px)', '0px')
);

test(
'should fold exact cancellation with large operands to zero, not a phantom',
testValue('calc(0.07px * 1e7 - 700000px)', '0px')
);

test(
'should reduce browser-prefixed calc (1)',
testValue('-webkit-calc(1px + 1px)', '2px')
Expand Down
5 changes: 5 additions & 0 deletions types/lib/simplify/bucket.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
export type UnitBucket = {
unit: string;
total: number;
/**
* largest |term| accumulated into `total`, for noise detection
*/
scale: number;
base: import("../convertUnits.js").BaseType | null;
order: number;
};
/**
* @typedef {object} UnitBucket
* @property {string} unit
* @property {number} total
* @property {number} scale largest |term| accumulated into `total`, for noise detection
* @property {import('../convertUnits.js').BaseType | null} base
* @property {number} order
*/
Expand Down
7 changes: 0 additions & 7 deletions types/lib/simplify/sum.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@ export type Sum = import("../node.js").Sum;
export type SumTerm = import("../node.js").SumTerm;
export type SimplifyFn = import("../simplify.js").SimplifyFn;
export type UnitBucket = import("./bucket.js").UnitBucket;
/**
* @typedef {import('../node.js').Node} Node
* @typedef {import('../node.js').Sum} Sum
* @typedef {import('../node.js').SumTerm} SumTerm
* @typedef {import('../simplify.js').SimplifyFn} SimplifyFn
* @typedef {import('./bucket.js').UnitBucket} UnitBucket
*/
/**
* @param {Sum} sum
* @param {SimplifyFn} simplify
Expand Down