Skip to content

Commit

Permalink
fix(legacy): InputNumber has rounding problems on blur with float l…
Browse files Browse the repository at this point in the history
…arge numbers
  • Loading branch information
nsbarsukov committed Dec 12, 2024
1 parent 16c4f3a commit b5dcfa9
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"import": ["@taiga-ui/cspell-config/cspell.config.js"],
"files": ["*/*.*"],
"ignorePaths": ["**/projects/i18n/languages/**", "**/addon-commerce/utils/get-currency-symbol.ts"],
"ignoreWords": ["Wachovia", "bottomsheet", "appbar", "qwertypgj_", "antialiasing", "xxxs"],
"ignoreWords": ["Wachovia", "bottomsheet", "appbar", "qwertypgj_", "antialiasing", "xxxs", "significand"],
"ignoreRegExpList": ["\\(https?://.*?\\)", "\\/{1}.+\\/{1}", "\\%2F.+", "\\%2C.+", "\\ɵ.+", "\\ыва.+"],
"overrides": [
{
Expand Down
20 changes: 17 additions & 3 deletions projects/cdk/utils/math/round.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,19 @@ function calculate(

precision = Math.min(precision, MAX_PRECISION);

const pair = `${value}e`.split('e');
const tempValue = func(Number(`${pair[0]}e${Number(pair[1]) + precision}`));
const processedPair = `${tempValue}e`.split('e');
const [significand, exponent = ''] = `${value}`.split('e');
const roundedInt = func(Number(`${significand}e${Number(exponent) + precision}`));

/**
* TODO: use BigInt after bumping Safari to 14+
*/
ngDevMode &&

Check warning on line 32 in projects/cdk/utils/math/round.ts

View check run for this annotation

codefactor.io / CodeFactor

projects/cdk/utils/math/round.ts#L32

Expected an assignment or function call and instead saw an expression. (@typescript-eslint/no-unused-expressions)
console.assert(
Number.isSafeInteger(roundedInt),
'Impossible to correctly round the such large number',
);

const processedPair = `${roundedInt}e`.split('e');

return Number(`${processedPair[0]}e${Number(processedPair[1]) - precision}`);
}
Expand All @@ -45,3 +55,7 @@ export function tuiFloor(value: number, precision = 0): number {
export function tuiTrunc(value: number, precision = 0): number {
return calculate(value, precision, Math.trunc);
}

export function tuiIsSafeToRound(value: number, precision = 0): boolean {
return Number.isSafeInteger(Math.trunc(value * 10 ** precision));
}
10 changes: 10 additions & 0 deletions projects/core/utils/format/test/format-number.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,14 @@ describe('Number formatting', () => {
}),
).toBe('0');
});

it('does not mutate value if precision is infinite', () => {
expect(
tuiFormatNumber(123_456_789_012_345.67, {
precision: Infinity,
thousandSeparator: ',',
rounding: 'round',
}),
).toBe('123,456,789,012,345.67');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ test.describe('InputNumber', () => {
await expect(example).toHaveScreenshot('01-input-number.png');
});

test('does not mutate already valid too large number on blur', async ({page}) => {
await tuiGoto(
page,
`${DemoRoute.InputNumber}/API?thousandSeparator=_&precision=2`,
);
await input.focus();
await input.clear();
await input.pressSequentially('123456789012345.6789');

await expect(input).toHaveValue('123_456_789_012_345.67');

await input.blur();

await expect(input).toHaveValue('123_456_789_012_345.67');

Check failure on line 45 in projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / macos-latest / 7 of 9

[webkit] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur

1) [webkit] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') Expected string: "123_456_789_012_345.67" Received string: "123_456_789_012_345.69" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') 8 × locator resolved to <input tabindex="0" maxlength="23" inputmode="decimal" aria-invalid="false" _ngcontent-ng-c3831396199="" id="tui_interactive_391601061540000" class="t-input ng-valid ng-dirty ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "123_456_789_012_345.69" 43 | await input.blur(); 44 | > 45 | await expect(input).toHaveValue('123_456_789_012_345.67'); | ^ 46 | }); 47 | 48 | test('prefix + value + postfix', async ({page}) => { at /Users/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts:45:33

Check failure on line 45 in projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / macos-latest / 7 of 9

[webkit] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur

1) [webkit] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') Expected string: "123_456_789_012_345.67" Received string: "123_456_789_012_345.69" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') 9 × locator resolved to <input tabindex="0" maxlength="23" inputmode="decimal" aria-invalid="false" _ngcontent-ng-c3831396199="" id="tui_interactive_391601061540000" class="t-input ng-valid ng-dirty ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "123_456_789_012_345.69" 43 | await input.blur(); 44 | > 45 | await expect(input).toHaveValue('123_456_789_012_345.67'); | ^ 46 | }); 47 | 48 | test('prefix + value + postfix', async ({page}) => { at /Users/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts:45:33

Check failure on line 45 in projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / macos-latest / 7 of 9

[webkit] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur

1) [webkit] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') Expected string: "123_456_789_012_345.67" Received string: "123_456_789_012_345.69" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') 9 × locator resolved to <input tabindex="0" maxlength="23" inputmode="decimal" aria-invalid="false" _ngcontent-ng-c3831396199="" id="tui_interactive_391601061540000" class="t-input ng-valid ng-dirty ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "123_456_789_012_345.69" 43 | await input.blur(); 44 | > 45 | await expect(input).toHaveValue('123_456_789_012_345.67'); | ^ 46 | }); 47 | 48 | test('prefix + value + postfix', async ({page}) => { at /Users/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts:45:33

Check failure on line 45 in projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / ubuntu-latest / 7 of 9

[chromium] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur

1) [chromium] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') Expected string: "123_456_789_012_345.67" Received string: "123_456_789_012_345.69" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') 9 × locator resolved to <input tabindex="0" maxlength="23" inputmode="decimal" aria-invalid="false" _ngcontent-ng-c3831396199="" id="tui_interactive_391601061540000" class="t-input ng-valid ng-dirty ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "123_456_789_012_345.69" 43 | await input.blur(); 44 | > 45 | await expect(input).toHaveValue('123_456_789_012_345.67'); | ^ 46 | }); 47 | 48 | test('prefix + value + postfix', async ({page}) => { at /home/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts:45:33

Check failure on line 45 in projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / ubuntu-latest / 7 of 9

[chromium] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur

1) [chromium] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') Expected string: "123_456_789_012_345.67" Received string: "123_456_789_012_345.69" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') 9 × locator resolved to <input tabindex="0" maxlength="23" inputmode="decimal" aria-invalid="false" _ngcontent-ng-c3831396199="" id="tui_interactive_391601061540000" class="t-input ng-valid ng-dirty ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "123_456_789_012_345.69" 43 | await input.blur(); 44 | > 45 | await expect(input).toHaveValue('123_456_789_012_345.67'); | ^ 46 | }); 47 | 48 | test('prefix + value + postfix', async ({page}) => { at /home/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts:45:33

Check failure on line 45 in projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright / ubuntu-latest / 7 of 9

[chromium] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur

1) [chromium] › tests/legacy/input-number/input-number.pw.spec.ts:32:13 › InputNumber › API › does not mutate already valid too large number on blur Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected) Locator: locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') Expected string: "123_456_789_012_345.67" Received string: "123_456_789_012_345.69" Call log: - expect.toHaveValue with timeout 5000ms - waiting for locator('#demo-content').getByTestId('tui-primitive-textfield__native-input') 9 × locator resolved to <input tabindex="0" maxlength="23" inputmode="decimal" aria-invalid="false" _ngcontent-ng-c3831396199="" id="tui_interactive_391601061540000" class="t-input ng-valid ng-dirty ng-touched" automation-id="tui-primitive-textfield__native-input"/> - unexpected value "123_456_789_012_345.69" 43 | await input.blur(); 44 | > 45 | await expect(input).toHaveValue('123_456_789_012_345.67'); | ^ 46 | }); 47 | 48 | test('prefix + value + postfix', async ({page}) => { at /home/runner/work/taiga-ui/taiga-ui/projects/demo-playwright/tests/legacy/input-number/input-number.pw.spec.ts:45:33
});

test('prefix + value + postfix', async ({page}) => {
await tuiGoto(
page,
Expand Down
12 changes: 10 additions & 2 deletions projects/legacy/components/input-number/input-number.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {TuiValueTransformer} from '@taiga-ui/cdk/classes';
import {CHAR_HYPHEN, CHAR_MINUS, EMPTY_QUERY} from '@taiga-ui/cdk/constants';
import {tuiWatch} from '@taiga-ui/cdk/observables';
import {TUI_IS_IOS} from '@taiga-ui/cdk/tokens';
import {tuiClamp} from '@taiga-ui/cdk/utils/math';
import {tuiClamp, tuiIsSafeToRound} from '@taiga-ui/cdk/utils/math';
import {tuiCreateToken, tuiPure} from '@taiga-ui/cdk/utils/miscellaneous';
import type {TuiDecimalMode} from '@taiga-ui/core/tokens';
import {TUI_DEFAULT_NUMBER_FORMAT, TUI_NUMBER_FORMAT} from '@taiga-ui/core/tokens';
Expand Down Expand Up @@ -274,7 +274,15 @@ export class TuiInputNumberComponent
this.computedPrefix +
tuiFormatNumber(value, {
...this.numberFormat,
precision: this.precision,
/**
* Number can satisfy interval [Number.MIN_SAFE_INTEGER; Number.MAX_SAFE_INTEGER]
* but its rounding can violate it.
* Before BigInt support there is no perfect solution – only trade off.
* No rounding is better than lose precision and incorrect mutation of already valid value.
*/
precision: tuiIsSafeToRound(value, this.precision)
? this.precision
: Infinity,
}).replace(CHAR_HYPHEN, CHAR_MINUS) +
this.computedPostfix
);
Expand Down

0 comments on commit b5dcfa9

Please sign in to comment.