Skip to content

Commit

Permalink
[IMP] formats: performance of splitNumber()
Browse files Browse the repository at this point in the history
To compute the formatted value of a number, we need to split it into
its integer digits and decimal digits. This was done by the function
`splitNumber()`, which was using Intl.NumberFormat, which handles
everything for us, but is quite slow.

Now convert the number to string using Number.toString(), and handle
the max number of decimal and the rounding by hand. We will still use
Intl.NumberFormat for numbers too big/small, for which Number.toString()
returns an exponential notation.

Rough benchmark :
Sheet 26 cols x 10.000 rows, using a CF on the whole sheet to force the
evaluation of the lazy values.
----------------------

w/ sheet filled with integers:
old: splitNumber ~299ms
new: splitNumber ~9.5ms

w/ sheet filled with floats:

Old: splitNumber ~555ms
New: splitNumber ~30ms
Task: 3272878
Part-of: #2373
  • Loading branch information
hokolomopo committed Apr 20, 2023
1 parent dd322bb commit a02ed72
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 6 deletions.
18 changes: 15 additions & 3 deletions demo/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -1514,12 +1514,22 @@ function computeFormulaCells(cols, rows) {
return cells;
}

function computeNumberCells(cols, rows) {
function computeNumberCells(cols, rows, type = "numbers") {
const cells = {};
for (let col = 0; col < cols; col++) {
const letter = _getColumnLetter(col);
for (let index = 1; index < rows - 1; index++) {
cells[letter + index] = { content: `${col + index}` };
switch (type) {
case "numbers":
cells[letter + index] = { content: `${col + index}` };
break;
case "floats":
cells[letter + index] = { content: `${col + index}.123` };
break;
case "longFloats":
cells[letter + index] = { content: `${col + index}.123456789123456` };
break;
}
}
}
return cells;
Expand Down Expand Up @@ -1552,7 +1562,9 @@ export function makeLargeDataset(cols, rows, sheetsInfo = ["formulas"]) {
cells = computeFormulaCells(cols, rows);
break;
case "numbers":
cells = computeNumberCells(cols, rows);
case "floats":
case "longFloats":
cells = computeNumberCells(cols, rows, sheetsInfo[0]);
break;
case "strings":
cells = computeStringCells(cols, rows);
Expand Down
92 changes: 89 additions & 3 deletions src/helpers/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,100 @@ const numberRepresentation: Intl.NumberFormat[] = [];
* - all digit stored in the decimal part of the number
*
* The 'maxDecimal' parameter allows to indicate the number of digits to not
* exceed in the decimal part, in which case digits are rounded
* exceed in the decimal part, in which case digits are rounded.
*
* Intl.Numberformat is used to properly handle all the roundings.
* e.g. 1234.7 with format ### (<> maxDecimals=0) should become 1235, not 1234
**/
function splitNumber(
value: number,
maxDecimals: number = MAX_DECIMAL_PLACES
): { integerDigits: string; decimalDigits: string | undefined } {
const asString = value.toString();
if (asString.includes("e")) return splitNumberIntl(value, maxDecimals);

if (Number.isInteger(value)) {
return { integerDigits: asString, decimalDigits: undefined };
}

const indexOfDot = asString.indexOf(".");
let integerDigits = asString.substring(0, indexOfDot);
let decimalDigits: string | undefined = asString.substring(indexOfDot + 1);

if (maxDecimals === 0) {
if (Number(decimalDigits[0]) >= 5) {
integerDigits = (Number(integerDigits) + 1).toString();
}
return { integerDigits, decimalDigits: undefined };
}

if (decimalDigits.length > maxDecimals) {
const { integerDigits: roundedIntegerDigits, decimalDigits: roundedDecimalDigits } =
limitDecimalDigits(decimalDigits, maxDecimals);

decimalDigits = roundedDecimalDigits;
if (roundedIntegerDigits !== "0") {
integerDigits = (Number(integerDigits) + Number(roundedIntegerDigits)).toString();
}
}

return { integerDigits, decimalDigits: removeTrailingZeroes(decimalDigits || "") };
}

/**
* Return the given string minus the trailing "0" characters.
*
* @param numberString : a string of integers
* @returns the numberString, minus the eventual zeroes at the end
*/
function removeTrailingZeroes(numberString: string): string | undefined {
let i = numberString.length - 1;
while (i >= 0 && numberString[i] === "0") {
i--;
}
return numberString.slice(0, i + 1) || undefined;
}

/**
* Limit the size of the decimal part of a number to the given number of digits.
*/
function limitDecimalDigits(
decimalDigits: string,
maxDecimals: number
): {
integerDigits: string;
decimalDigits: string | undefined;
} {
let integerDigits = "0";
let resultDecimalDigits: string | undefined = decimalDigits;

// Note : we'd want to simply use number.toFixed() to handle the max digits & rounding,
// but it has very strange behaviour. Ex: 12.345.toFixed(2) => "12.35", but 1.345.toFixed(2) => "1.34"
let slicedDecimalDigits = decimalDigits.slice(0, maxDecimals);
const i = maxDecimals;

if (Number(Number(decimalDigits[i]) < 5)) {
return { integerDigits, decimalDigits: slicedDecimalDigits };
}

// round up
const slicedRoundedUp = (Number(slicedDecimalDigits) + 1).toString();
if (slicedRoundedUp.length > slicedDecimalDigits.length) {
integerDigits = (Number(integerDigits) + 1).toString();
resultDecimalDigits = undefined;
} else {
resultDecimalDigits = slicedRoundedUp;
}

return { integerDigits, decimalDigits: resultDecimalDigits };
}

/**
* Split numbers into decimal/integer digits using Intl.NumberFormat.
* Supports numbers with a lot of digits that are transformed to scientific notation by
* number.toString(), but is slow.
*/
function splitNumberIntl(
value: number,
maxDecimals: number = MAX_DECIMAL_PLACES
): { integerDigits: string; decimalDigits: string | undefined } {
let formatter = numberRepresentation[maxDecimals];
if (!formatter) {
Expand Down
6 changes: 6 additions & 0 deletions tests/helpers/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ describe("formatValue on number", () => {
expect(formatValue(-0.00000000001)).toBe("-0");
expect(formatValue(-0.000000000001)).toBe("-0");

expect(formatValue(0.9999999)).toBe("0.9999999");
expect(formatValue(0.99999999)).toBe("0.99999999");
expect(formatValue(0.999999999)).toBe("0.999999999");
expect(formatValue(0.9999999999)).toBe("1");
expect(formatValue(0.99999999999)).toBe("1");

expect(formatValue(1.123456789)).toBe("1.123456789");
expect(formatValue(10.123456789)).toBe("10.12345679");
expect(formatValue(100.123456789)).toBe("100.1234568");
Expand Down

0 comments on commit a02ed72

Please sign in to comment.