Skip to content

fix(input, input-number): increment/decrement unsafe numbers without loss of precision #6580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 14, 2023

Conversation

benelan
Copy link
Member

@benelan benelan commented Mar 9, 2023

Related Issue: #5920

Summary

Numbers that cannot be represented in JavaScript's IEEE-754 double precision were being rounded when incrementing/decrementing because they were cast as Number (we store the value as a string). We use BigDecimal class to support numbers that do not fit into JavaScript's encoding method. I had overlooked the increment/decrement functionality when implementing BigDecimal.

BigDecimal uses BigInt internally, and unfortunately BigInt doesn't support exponential notation. For example, BigInt("1e2") throws the error:

Uncaught SyntaxError: invalid BigInt syntax

To work around the constraint, I manually expand any exponential notation numbers into normal decimal numbers. All of this is done via string manipulation, so sorry in advance for the burned retinas you are about to receive from this PR review.

For more info:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger#description

benelan and others added 8 commits November 30, 2022 13:07
…ment-bigdecimal

* origin/master:
  test(tabs): delay story due to potential timing chromatic diff (#6437)
  build(deps): Bump chromatic from 6.14.0 to 6.17.1 (#6571)
  build(deps): Bump postcss from 8.4.20 to 8.4.21 (#6570)
  build(deps): Bump eslint-config-prettier from 8.6.0 to 8.7.0 (#6572)
  ci(eslint): ignore private/internal code for jsdoc rules (#6416)
@benelan benelan requested a review from a team as a code owner March 9, 2023 00:21
@github-actions github-actions bot added the bug Bug reports for broken functionality. Issues should include a reproduction of the bug. label Mar 9, 2023
Copy link
Member

@jcfranco jcfranco left a comment

Choose a reason for hiding this comment

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

✨🔢🔧✨

@@ -751,7 +761,7 @@ export class InputNumber
event.stopPropagation();
};

private setChildNumberElRef = (el) => {
private setChildNumberElRef = (el: HTMLInputElement) => {
Copy link
Member

Choose a reason for hiding this comment

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

✨🏆✨

const decimalNumberRegex = new RegExp(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);

export const decimalPlaces = (value: string): number => {
const match = value.match(decimalNumberRegex);
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: would test help simplify this?

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to use the values of the match a couple lines down

@@ -12,6 +12,8 @@ describe("calcite-tooltip", () => {
/**
* Helps assert the canceled Esc key press when closing tooltips
* Must be called before the tooltip is closed via keyboard.
*
* @param page
Copy link
Member

Choose a reason for hiding this comment

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

Can you roll this file back? I think it was auto-fixed by the linter, but the file is unrelated to this PR.

@@ -1200,7 +1200,7 @@ export class Slider
const percent = (x - left) / width;
const mirror = this.shouldMirror();
const clampedValue = this.clamp(this.min + range * (mirror ? 1 - percent : percent));
let value = Number(clampedValue.toFixed(decimalPlaces(this.step)));
let value = Number(clampedValue.toFixed(decimalPlaces(`${this.step}`)));
Copy link
Member

Choose a reason for hiding this comment

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

If this fixes precision on the slider, can you update the PR title to reflect this? If it's to fix types, can you submit this in a separate PR? If it's the latter, we can merge the separate PR first and we can skip rolling it back here.

Copy link
Member Author

Choose a reason for hiding this comment

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

The parameter needs to be a string so the decimalPlaces function can also be used with BigDecimal. Previously step was being converted to a string in the decimalPlaces function itself, but we would lose precision if converting BigDecimal to number before passing it to the function. So it doesn't fix anything in slider, it's just a small, required refactor.

Copy link
Member Author

Choose a reason for hiding this comment

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

Nevermind, Input isn't using decimalPlaces anymore, removing!

if (exponentialParts.length === 1) {
return numberString;
}
if (Number.isSafeInteger(+numberString)) {
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: for readability, can you add a newline between if statements?

if (exponentialParts.length === 1) {
return numberString;
}
if (Number.isSafeInteger(+numberString)) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you store +numberString in a local variable and reuse it?


const inputValPlaces = decimalPlaces(inputVal);
const inputStepPlaces = decimalPlaces(inputStep);
const finalValue = nudgedValueBelowInputMin
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: can this be refactored to compute and evaluate a nudged value check at a time? It's currently computing two values where one might possibly be thrown away.

benelan added 4 commits March 13, 2023 11:20
…ment-bigdecimal

* origin/master:
  build(deps): Bump rimraf and @types/rimraf (#6541)
  build(deps): Bump quicktype-core from 6.1.0 to 23.0.12 (#6573)
…calcite-components into benelan/input-increment-bigdecimal

* 'benelan/input-increment-bigdecimal' of github.com:esri/calcite-components:
  chore: cleanup
  fix: remove BigDecimal.toFixed() method
  style: format
@benelan benelan added the pr ready for visual snapshots Adding this label will run visual snapshot testing. label Mar 13, 2023
@benelan benelan merged commit 40c0f0f into master Mar 14, 2023
@benelan benelan deleted the benelan/input-increment-bigdecimal branch March 14, 2023 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Bug reports for broken functionality. Issues should include a reproduction of the bug. pr ready for visual snapshots Adding this label will run visual snapshot testing.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants