Compare commits
1
Commits
7b2bd8d379
...
f1ccd3c7ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ccd3c7ae |
@@ -56,6 +56,16 @@ describe("shared display helpers", () => {
|
||||
)).toBe("0,00 ₽");
|
||||
});
|
||||
|
||||
it("rounds to the currency's minor units and drops the sign of a rounded zero", () => {
|
||||
const rounded = { withRoundedCents: true };
|
||||
expect(formatMoney({ currency: "RUB", value: "-0.004" }, rounded)).toBe("0,00 ₽");
|
||||
expect(formatMoney({ currency: "RUB", value: "-0.005" }, rounded)).toBe("-0,01 ₽");
|
||||
expect(formatMoney({ currency: "RUB", value: "999.995" }, rounded)).toBe("1 000,00 ₽");
|
||||
expect(formatMoney({ currency: "JPY", value: "1500.5" }, rounded)).toBe("1 501 JPY");
|
||||
expect(formatMoney({ currency: "KWD", value: "1.2345" }, rounded)).toBe("1,235 KWD");
|
||||
expect(formatMoney({ currency: "ZZZ", value: "1.234" }, rounded)).toBe("1,23 ZZZ");
|
||||
});
|
||||
|
||||
it("builds the brand logo url from an upstream file name", () => {
|
||||
expect(instrumentLogoUrl("sber3.png")).toBe(
|
||||
"https://invest-brands.cdn-tinkoff.ru/sber3x160.png",
|
||||
|
||||
+31
-14
@@ -1,5 +1,5 @@
|
||||
const DECIMAL_PART_COUNT = 2;
|
||||
const CENTS_PRECISION = 2;
|
||||
const DEFAULT_MINOR_UNITS = 2;
|
||||
|
||||
type FormatMoneyOptions = {
|
||||
withRoundedCents?: boolean;
|
||||
@@ -23,6 +23,17 @@ const dateFormatter = new Intl.DateTimeFormat("ru-RU", {
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
// ISO 4217 minor units (JPY 0, KWD 3); unknown codes fall back to 2.
|
||||
function currencyMinorUnits(currency: string): number {
|
||||
try {
|
||||
return new Intl.NumberFormat("en", { currency, style: "currency" })
|
||||
.resolvedOptions().maximumFractionDigits ?? DEFAULT_MINOR_UNITS;
|
||||
}
|
||||
catch {
|
||||
return DEFAULT_MINOR_UNITS;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
return dateFormatter.format(new Date(value));
|
||||
}
|
||||
@@ -56,8 +67,10 @@ function formatMoney(
|
||||
): string {
|
||||
if (!money) return "—";
|
||||
|
||||
const minorUnits = currencyMinorUnits(money.currency);
|
||||
|
||||
const value = withRoundedCents
|
||||
? roundDecimalToCents(money.value)
|
||||
? roundDecimal(money.value, minorUnits)
|
||||
: money.value;
|
||||
|
||||
const [integerPart = "", fractionPart = ""] = value.split(
|
||||
@@ -66,7 +79,7 @@ function formatMoney(
|
||||
);
|
||||
|
||||
const normalizedFraction = withRoundedCents || withZeroCents
|
||||
? fractionPart.padEnd(CENTS_PRECISION, "0")
|
||||
? fractionPart.padEnd(minorUnits, "0")
|
||||
: fractionPart;
|
||||
|
||||
const decimal = normalizedFraction
|
||||
@@ -95,21 +108,25 @@ function isZeroDecimal(value: string) {
|
||||
&& fraction.replaceAll("0", "") === "";
|
||||
}
|
||||
|
||||
function roundDecimalToCents(value: string): string {
|
||||
function roundDecimal(value: string, places: number): string {
|
||||
const [integer = "", fraction = ""] = value.split(".", DECIMAL_PART_COUNT);
|
||||
const sign = integer.startsWith("-") ? "-" : "";
|
||||
const digits = sign ? integer.slice(1) : integer;
|
||||
const roundedFraction = fraction.padEnd(CENTS_PRECISION + 1, "0");
|
||||
const cents = `${digits}${roundedFraction.slice(0, CENTS_PRECISION)}`;
|
||||
const roundingDigit = roundedFraction.at(CENTS_PRECISION) ?? "0";
|
||||
const digits = integer.startsWith("-") ? integer.slice(1) : integer;
|
||||
const paddedFraction = fraction.padEnd(places + 1, "0");
|
||||
const units = `${digits}${paddedFraction.slice(0, places)}`;
|
||||
const roundingDigit = paddedFraction.at(places) ?? "0";
|
||||
|
||||
const rounded = roundingDigit >= "5"
|
||||
? (BigInt(cents) + 1n).toString().padStart(cents.length, "0")
|
||||
: cents;
|
||||
const rounded = (roundingDigit >= "5" ? BigInt(units) + 1n : BigInt(units))
|
||||
.toString()
|
||||
.padStart(places + 1, "0");
|
||||
|
||||
const roundedInteger = rounded.slice(0, -CENTS_PRECISION) || "0";
|
||||
// A value rounded to zero has no sign: "-0.004" → "0.00".
|
||||
const sign = integer.startsWith("-") && /[1-9]/.test(rounded) ? "-" : "";
|
||||
const roundedInteger = rounded.slice(0, rounded.length - places);
|
||||
const roundedFraction = rounded.slice(rounded.length - places);
|
||||
|
||||
return `${sign}${roundedInteger}.${rounded.slice(-CENTS_PRECISION)}`;
|
||||
return roundedFraction
|
||||
? `${sign}${roundedInteger}.${roundedFraction}`
|
||||
: `${sign}${roundedInteger}`;
|
||||
}
|
||||
|
||||
// Без timeZone Intl берёт часовой пояс браузера пользователя.
|
||||
|
||||
Reference in New Issue
Block a user