fix(twenty-shared): derive short-number suffix from the rounded value (#21591)

`formatToShortNumber`
(`packages/twenty-shared/src/utils/format/formatToShortNumber.ts`)
picked the unit suffix from the **raw** value but printed the
**rounded** figure, so `999999` rendered as `"1000k"` instead of `"1m"`,
and `999999999` as `"1000m"` instead of `"1b"`. This affects
number/currency cells, column-footer aggregates, and dashboard charts.

The fix replaces the hard-coded band branches with a promotion loop that
derives the suffix from the rounded display value, so the suffix and
figure always agree at boundaries. Adds boundary, just-below-boundary,
and negative-boundary tests.

Red-green proven: the two new boundary tests fail on the original source
(`expected "1m" but got "1000k"`); the 11 pre-existing tests still pass;
all 13 pass with the fix. Verified with a standalone strict `tsc` (0
errors) and oxlint on both changed files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21591?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Yash Singh
2026-06-19 11:59:47 +05:30
committed by GitHub
parent 814b43ca41
commit 505094650f
2 changed files with 25 additions and 18 deletions
@@ -30,6 +30,17 @@ describe('formatToShortNumber', () => {
expect(formatToShortNumber(0)).toBe('0');
});
it('promotes to the next unit when rounding reaches the boundary', () => {
expect(formatToShortNumber(999999)).toBe('1m');
expect(formatToShortNumber(999950)).toBe('1m');
expect(formatToShortNumber(999999999)).toBe('1b');
});
it('does not promote just below a rounding boundary', () => {
expect(formatToShortNumber(999949)).toBe('999.9k');
expect(formatToShortNumber(999)).toBe('999');
});
describe('negative numbers', () => {
it('formats negative numbers less than 1000 correctly', () => {
expect(formatToShortNumber(-500)).toBe('-500');
@@ -50,5 +61,9 @@ describe('formatToShortNumber', () => {
expect(formatToShortNumber(-1200000000)).toBe('-1.2b');
expect(formatToShortNumber(-987654321987)).toBe('-987.7b');
});
it('promotes negative numbers when rounding reaches the boundary', () => {
expect(formatToShortNumber(-999999)).toBe('-1m');
});
});
});
@@ -2,25 +2,17 @@ export const formatToShortNumber = (amount: number) => {
const sign = amount < 0 ? '-' : '';
const absoluteAmount = Math.abs(amount);
if (absoluteAmount < 1000) {
return sign + absoluteAmount.toFixed(1).replace(/\.?0+$/, '');
}
const format = (scaled: number, suffix: string) =>
sign + scaled.toFixed(1).replace(/\.?0+$/, '') + suffix;
if (absoluteAmount < 1_000_000) {
return (
sign + (absoluteAmount / 1000).toFixed(1).replace(/\.?0+$/, '') + 'k'
);
if (Number(absoluteAmount.toFixed(1)) < 1000) {
return format(absoluteAmount, '');
}
if (absoluteAmount < 1_000_000_000) {
return (
sign + (absoluteAmount / 1_000_000).toFixed(1).replace(/\.?0+$/, '') + 'm'
);
if (Number((absoluteAmount / 1_000).toFixed(1)) < 1000) {
return format(absoluteAmount / 1_000, 'k');
}
return (
sign +
(absoluteAmount / 1_000_000_000).toFixed(1).replace(/\.?0+$/, '') +
'b'
);
if (Number((absoluteAmount / 1_000_000).toFixed(1)) < 1000) {
return format(absoluteAmount / 1_000_000, 'm');
}
return format(absoluteAmount / 1_000_000_000, 'b');
};