Skip to content
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

fix(input, input-number): decimals no longer contain groupSeparators and remove leading zeros #5490

Merged
merged 10 commits into from
Nov 16, 2022
29 changes: 22 additions & 7 deletions src/utils/locale.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
locales,
numberingSystems,
numberStringFormatter,
defaultLocale,
defaultNumberingSystem,
Expand Down Expand Up @@ -35,7 +36,7 @@ describe("NumberStringFormat", () => {
});

locales.forEach((locale) => {
it(`integers localize and delocalize in "${locale}"`, () => {
it(`locale: integers localize and delocalize in "${locale}"`, () => {
Copy link
Member

Choose a reason for hiding this comment

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

You could place these in a describe("locale", () => { /*...*/ block for grouping instead of updating the individual test names.

const numberString = "555";
numberStringFormatter.numberFormatOptions = {
locale,
Expand All @@ -47,7 +48,7 @@ describe("NumberStringFormat", () => {
expect(delocalizedNumberString).toBe(numberString);
});

it(`negative numbers localize and delocalize in "${locale}"`, () => {
it(`locale: negative numbers localize and delocalize in "${locale}"`, () => {
const numberString = "-123";
numberStringFormatter.numberFormatOptions = {
locale,
Expand All @@ -59,7 +60,7 @@ describe("NumberStringFormat", () => {
expect(delocalizedNumberString).toBe(numberString);
});

it(`floating point numbers localize and delocalize in "${locale}"`, () => {
it(`locale: floating point numbers localize and delocalize in "${locale}"`, () => {
const numberString = "4.321";
numberStringFormatter.numberFormatOptions = {
locale,
Expand All @@ -71,7 +72,7 @@ describe("NumberStringFormat", () => {
expect(delocalizedNumberString).toBe(numberString);
});

it(`exponential numbers localize and delocalize in "${locale}"`, () => {
it(`locale: exponential numbers localize and delocalize in "${locale}"`, () => {
const numberString = "2.5e-3";
numberStringFormatter.numberFormatOptions = {
locale,
Expand All @@ -83,7 +84,7 @@ describe("NumberStringFormat", () => {
expect(delocalizedNumberString).toBe(numberString);
});

it(`numbers with group separators localize and delocalize in "${locale}"`, () => {
it(`locale: numbers with group separators localize and delocalize in "${locale}"`, () => {
const numberString = "1234567890";
numberStringFormatter.numberFormatOptions = {
locale,
Expand All @@ -96,8 +97,8 @@ describe("NumberStringFormat", () => {
expect(delocalizedNumberString).toBe(numberString);
});

it(`floating point numbers with group separators localize and delocalize in "${locale}"`, () => {
const numberString = "12345678.9";
it(`locale: floating point numbers with group separators localize and delocalize in "${locale}"`, () => {
const numberString = "12345678.0123456789";
numberStringFormatter.numberFormatOptions = {
locale,
// the group separator is different in arabic depending on the numberingSystem
Expand All @@ -109,4 +110,18 @@ describe("NumberStringFormat", () => {
expect(delocalizedNumberString).toBe(numberString);
});
});

numberingSystems.forEach((numberingSystem) => {
const numberString = "0.0123456789";
it(`numberingSystem: floating point numbers with group separators localize and delocalize in "${numberingSystem}"`, () => {
numberStringFormatter.numberFormatOptions = {
locale: numberingSystem === "arab" ? "ar" : "en",
numberingSystem,
useGrouping: true
};
const localizedNumberString = numberStringFormatter.localize(numberString);
const delocalizedNumberString = numberStringFormatter.delocalize(localizedNumberString);
expect(delocalizedNumberString).toBe(numberString);
});
});
});
8 changes: 4 additions & 4 deletions src/utils/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ export interface NumberStringFormatOptions extends Intl.NumberFormatOptions {
/**
* This util formats and parses numbers for localization
*/
class NumberStringFormat {
export class NumberStringFormat {
/**
* The actual group separator for the specified locale.
* Some white space group separators don't render correctly in the browser,
Expand Down Expand Up @@ -350,7 +350,7 @@ class NumberStringFormat {
this._getDigitIndex = (d: string) => index.get(d);
}

delocalize = (numberString: string) =>
delocalize = (numberString: string): string =>
// For performance, (de)localization is skipped if the formatter isn't initialized.
// In order to localize/delocalize, e.g. when lang/numberingSystem props are not default values,
// `numberFormatOptions` must be set in a component to create and cache the formatter.
Expand All @@ -365,12 +365,12 @@ class NumberStringFormat {
)
: numberString;

localize = (numberString: string) =>
localize = (numberString: string): string =>
this._numberFormatOptions
? sanitizeExponentialNumberString(numberString, (nonExpoNumString: string): string =>
isValidNumber(nonExpoNumString.trim())
? new BigDecimal(nonExpoNumString.trim())
.format(this._numberFormatter)
.format(this)
.replace(new RegExp(`[${this._actualGroup}]`, "g"), this._group)
: nonExpoNumString
)
Expand Down
11 changes: 10 additions & 1 deletion src/utils/number.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ describe("BigDecimal", () => {
expect(negativeZero).toBe("-0");
});

it("correctly formats long decimal numbers", () => {
numberStringFormatter.numberFormatOptions = {
locale: "en",
numberingSystem: "latn",
useGrouping: true
};
expect(new BigDecimal("123.0123456789").format(numberStringFormatter)).toBe("123.0123456789");
});

locales.forEach((locale) => {
it(`correctly localizes number parts - ${locale}`, () => {
numberStringFormatter.numberFormatOptions = {
Expand All @@ -120,7 +129,7 @@ describe("BigDecimal", () => {
useGrouping: true
};

const parts = new BigDecimal("-12345678.9").formatToParts(numberStringFormatter.numberFormatter);
const parts = new BigDecimal("-12345678.9").formatToParts(numberStringFormatter);
const groupPart = parts.find((part) => part.type === "group").value;
expect(groupPart.trim().length === 0 ? " " : groupPart).toBe(numberStringFormatter.group);
expect(parts.find((part) => part.type === "decimal").value).toBe(numberStringFormatter.decimal);
Expand Down
90 changes: 39 additions & 51 deletions src/utils/number.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { numberKeys } from "./key";
import { numberStringFormatter } from "./locale";
import { NumberStringFormat } from "./locale";

const defaultMinusSignRegex = new RegExp("-", "g");
const unnecessaryDecimalRegex = new RegExp("\\.?0+$");

// adopted from https://stackoverflow.com/a/66939244
export class BigDecimal {
Expand Down Expand Up @@ -27,77 +30,62 @@ export class BigDecimal {
this.isNegative = input.charAt(0) === "-";
}

static _divRound(dividend: bigint, divisor: bigint): bigint {
return BigDecimal.fromBigInt(
static _divRound = (dividend: bigint, divisor: bigint): bigint =>
BigDecimal.fromBigInt(
dividend / divisor + (BigDecimal.ROUNDED ? ((dividend * BigInt(2)) / divisor) % BigInt(2) : BigInt(0))
);
}

static fromBigInt(bigint: bigint): bigint {
return Object.assign(Object.create(BigDecimal.prototype), { value: bigint });
}
static fromBigInt = (bigint: bigint): bigint => Object.assign(Object.create(BigDecimal.prototype), { value: bigint });

toString(): string {
getIntegersAndDecimals(): { integers: string; decimals: string } {
const s = this.value
.toString()
.replace(new RegExp("-", "g"), "")
.replace(defaultMinusSignRegex, "")
.padStart(BigDecimal.DECIMALS + 1, "0");

const i = s.slice(0, -BigDecimal.DECIMALS);
const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
const value = i.concat(d.length ? "." + d : "");
return `${this.isNegative ? "-" : ""}${value}`;
const integers = s.slice(0, -BigDecimal.DECIMALS);
const decimals = s.slice(-BigDecimal.DECIMALS).replace(unnecessaryDecimalRegex, "");
return { integers, decimals };
}

formatToParts(formatter: Intl.NumberFormat): Intl.NumberFormatPart[] {
const s = this.value
.toString()
.replace(new RegExp("-", "g"), "")
.padStart(BigDecimal.DECIMALS + 1, "0");

const i = s.slice(0, -BigDecimal.DECIMALS);
const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");
toString(): string {
const { integers, decimals } = this.getIntegersAndDecimals();
return `${this.isNegative ? "-" : ""}${integers}${decimals.length ? "." + decimals : ""}`;
}

const parts = formatter.formatToParts(BigInt(i));
this.isNegative && parts.unshift({ type: "minusSign", value: numberStringFormatter.minusSign });
formatToParts(formatter: NumberStringFormat): Intl.NumberFormatPart[] {
const { integers, decimals } = this.getIntegersAndDecimals();
const parts = formatter.numberFormatter.formatToParts(BigInt(integers));
this.isNegative && parts.unshift({ type: "minusSign", value: formatter.minusSign });

if (d.length) {
parts.push({ type: "decimal", value: numberStringFormatter.decimal });
d.split("").forEach((char: string) => parts.push({ type: "fraction", value: char }));
if (decimals.length) {
parts.push({ type: "decimal", value: formatter.decimal });
decimals.split("").forEach((char: string) => parts.push({ type: "fraction", value: char }));
}

return parts;
}

format(formatter: Intl.NumberFormat): string {
const s = this.value
.toString()
.replace(new RegExp("-", "g"), "")
.padStart(BigDecimal.DECIMALS + 1, "0");

const i = s.slice(0, -BigDecimal.DECIMALS);
const d = s.slice(-BigDecimal.DECIMALS).replace(/\.?0+$/, "");

const iFormatted = `${this.isNegative ? numberStringFormatter.minusSign : ""}${formatter.format(BigInt(i))}`;
const dFormatted = d.length ? `${numberStringFormatter.decimal}${formatter.format(BigInt(d))}` : "";
return `${iFormatted}${dFormatted}`;
format(formatter: NumberStringFormat): string {
const { integers, decimals } = this.getIntegersAndDecimals();
const integersFormatted = `${this.isNegative ? formatter.minusSign : ""}${formatter.numberFormatter.format(
BigInt(integers)
)}`;
const decimalsFormatted = decimals.length
? `${formatter.decimal}${decimals
.split("")
.map((char: string) => formatter.numberFormatter.format(Number(char)))
.join("")}`
: "";
return `${integersFormatted}${decimalsFormatted}`;
}

add(num: string): bigint {
return BigDecimal.fromBigInt(this.value + new BigDecimal(num).value);
}
add = (num: string): bigint => BigDecimal.fromBigInt(this.value + new BigDecimal(num).value);

subtract(num: string): bigint {
return BigDecimal.fromBigInt(this.value - new BigDecimal(num).value);
}
subtract = (num: string): bigint => BigDecimal.fromBigInt(this.value - new BigDecimal(num).value);

multiply(num: string): bigint {
return BigDecimal._divRound(this.value * new BigDecimal(num).value, BigDecimal.SHIFT);
}
multiply = (num: string): bigint => BigDecimal._divRound(this.value * new BigDecimal(num).value, BigDecimal.SHIFT);

divide(num: string): bigint {
return BigDecimal._divRound(this.value * BigDecimal.SHIFT, new BigDecimal(num).value);
}
divide = (num: string): bigint => BigDecimal._divRound(this.value * BigDecimal.SHIFT, new BigDecimal(num).value);
}

export function isValidNumber(numberString: string): boolean {
Expand Down