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
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.123456789").format(numberStringFormatter)).toBe("123.123456789");
benelan marked this conversation as resolved.
Show resolved Hide resolved
});

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