mirror of
https://github.com/pezkuwichain/pezkuwi-common.git
synced 2026-10-20 18:56:18 +00:00
util: group thousands in linear time
formatDecimal split digits with /(\d+?)(?=(\d{3})+(?!\d)|$)/g, whose
lookahead rescans the rest of a digit run at every position: quadratic in
the run's length (CodeQL js/polynomial-redos, high; inherited unchanged
from upstream). 20k digits took 258 ms, 200k about 25 s.
A loop now finds the same groups: within each digit run a group ends where
the digits left are a positive multiple of three, or at the end of the
string. Identical output to the pattern on 210,000 random inputs plus the
edge cases (letters around digits, short runs, negatives); 200k digits take
7 ms. The new spec case for a 60k-digit number fails on the old code.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ad3816da8e
commit
edd1ea0870
2 files changed
+75
-6
No files matched your search
@@ -25,4 +25,21 @@ describe('formatDecimal', (): void => {
|
||||
it('formats negative numbers', (): void => {
|
||||
expect(formatDecimal('-123456')).toEqual('-123,456');
|
||||
});
|
||||
|
||||
// The pattern this replaced took quadratic time on a long digit run
|
||||
// (CodeQL js/polynomial-redos): 60k digits took seconds; this is linear.
|
||||
it('formats a very long number quickly', (): void => {
|
||||
const start = Date.now();
|
||||
const out = formatDecimal(`1${'0'.repeat(60_000)}`);
|
||||
|
||||
expect(Date.now() - start < 1000).toEqual(true);
|
||||
expect(out.length).toEqual(60_001 + 20_000);
|
||||
expect(out.startsWith('1,000,000')).toEqual(true);
|
||||
});
|
||||
|
||||
it('keeps the grouping of the pattern it replaced', (): void => {
|
||||
expect(formatDecimal('ab123cd')).toEqual('ab123cd');
|
||||
expect(formatDecimal('12a3456')).toEqual('3,456');
|
||||
expect(formatDecimal('1000000', '_')).toEqual('1_000_000');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,62 @@
|
||||
// Copyright 2017-2026 @pezkuwi/util authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// eslint-disable-next-line prefer-regex-literals
|
||||
const NUMBER_REGEX = new RegExp('(\\d+?)(?=(\\d{3})+(?!\\d)|$)', 'g');
|
||||
function isDigit (c: string): boolean {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the digits of value into thousands groups, exactly as the pattern
|
||||
* /(\d+?)(?=(\d{3})+(?!\d)|$)/g matched them, without its quadratic cost
|
||||
* on long digit runs (CodeQL js/polynomial-redos): within each run of digits,
|
||||
* a group ends where the digits left in the run are a positive multiple of
|
||||
* three, or at the end of the string. A run that never reaches such a point
|
||||
* (two digits followed by a letter, say) contributes nothing.
|
||||
*/
|
||||
function groups (value: string): string[] {
|
||||
const out: string[] = [];
|
||||
const end = value.length;
|
||||
let i = 0;
|
||||
|
||||
while (i < end) {
|
||||
if (!isDigit(value[i])) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let j = i;
|
||||
|
||||
while (j < end && isDigit(value[j])) {
|
||||
j++;
|
||||
}
|
||||
|
||||
let start = i;
|
||||
|
||||
while (start < j) {
|
||||
let stop = -1;
|
||||
|
||||
for (let q = start + 1; q <= j; q++) {
|
||||
const left = j - q;
|
||||
|
||||
if ((left > 0 && left % 3 === 0) || (left === 0 && j === end)) {
|
||||
stop = q;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stop === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
out.push(value.slice(start, stop));
|
||||
start = stop;
|
||||
}
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name formatDecimal
|
||||
@@ -12,11 +66,9 @@ export function formatDecimal (value: string, separator = ','): string {
|
||||
// We can do this by adjusting the regx, however for the sake of clarity
|
||||
// we rather strip and re-add the negative sign in the output
|
||||
const isNegative = value[0].startsWith('-');
|
||||
const matched = isNegative
|
||||
? value.substring(1).match(NUMBER_REGEX)
|
||||
: value.match(NUMBER_REGEX);
|
||||
const matched = groups(isNegative ? value.substring(1) : value);
|
||||
|
||||
return matched
|
||||
return matched.length
|
||||
? `${isNegative ? '-' : ''}${matched.join(separator)}`
|
||||
: value;
|
||||
}
|
||||
Reference in new issue
Block a user