Compare commits
1
Commits
v1.4.0
...
f1ccd3c7ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ccd3c7ae |
@@ -56,6 +56,16 @@ describe("shared display helpers", () => {
|
|||||||
)).toBe("0,00 ₽");
|
)).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", () => {
|
it("builds the brand logo url from an upstream file name", () => {
|
||||||
expect(instrumentLogoUrl("sber3.png")).toBe(
|
expect(instrumentLogoUrl("sber3.png")).toBe(
|
||||||
"https://invest-brands.cdn-tinkoff.ru/sber3x160.png",
|
"https://invest-brands.cdn-tinkoff.ru/sber3x160.png",
|
||||||
|
|||||||
+31
-14
@@ -1,5 +1,5 @@
|
|||||||
const DECIMAL_PART_COUNT = 2;
|
const DECIMAL_PART_COUNT = 2;
|
||||||
const CENTS_PRECISION = 2;
|
const DEFAULT_MINOR_UNITS = 2;
|
||||||
|
|
||||||
type FormatMoneyOptions = {
|
type FormatMoneyOptions = {
|
||||||
withRoundedCents?: boolean;
|
withRoundedCents?: boolean;
|
||||||
@@ -23,6 +23,17 @@ const dateFormatter = new Intl.DateTimeFormat("ru-RU", {
|
|||||||
year: "numeric",
|
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 {
|
function formatDate(value: string): string {
|
||||||
return dateFormatter.format(new Date(value));
|
return dateFormatter.format(new Date(value));
|
||||||
}
|
}
|
||||||
@@ -56,8 +67,10 @@ function formatMoney(
|
|||||||
): string {
|
): string {
|
||||||
if (!money) return "—";
|
if (!money) return "—";
|
||||||
|
|
||||||
|
const minorUnits = currencyMinorUnits(money.currency);
|
||||||
|
|
||||||
const value = withRoundedCents
|
const value = withRoundedCents
|
||||||
? roundDecimalToCents(money.value)
|
? roundDecimal(money.value, minorUnits)
|
||||||
: money.value;
|
: money.value;
|
||||||
|
|
||||||
const [integerPart = "", fractionPart = ""] = value.split(
|
const [integerPart = "", fractionPart = ""] = value.split(
|
||||||
@@ -66,7 +79,7 @@ function formatMoney(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const normalizedFraction = withRoundedCents || withZeroCents
|
const normalizedFraction = withRoundedCents || withZeroCents
|
||||||
? fractionPart.padEnd(CENTS_PRECISION, "0")
|
? fractionPart.padEnd(minorUnits, "0")
|
||||||
: fractionPart;
|
: fractionPart;
|
||||||
|
|
||||||
const decimal = normalizedFraction
|
const decimal = normalizedFraction
|
||||||
@@ -95,21 +108,25 @@ function isZeroDecimal(value: string) {
|
|||||||
&& fraction.replaceAll("0", "") === "";
|
&& fraction.replaceAll("0", "") === "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundDecimalToCents(value: string): string {
|
function roundDecimal(value: string, places: number): string {
|
||||||
const [integer = "", fraction = ""] = value.split(".", DECIMAL_PART_COUNT);
|
const [integer = "", fraction = ""] = value.split(".", DECIMAL_PART_COUNT);
|
||||||
const sign = integer.startsWith("-") ? "-" : "";
|
const digits = integer.startsWith("-") ? integer.slice(1) : integer;
|
||||||
const digits = sign ? integer.slice(1) : integer;
|
const paddedFraction = fraction.padEnd(places + 1, "0");
|
||||||
const roundedFraction = fraction.padEnd(CENTS_PRECISION + 1, "0");
|
const units = `${digits}${paddedFraction.slice(0, places)}`;
|
||||||
const cents = `${digits}${roundedFraction.slice(0, CENTS_PRECISION)}`;
|
const roundingDigit = paddedFraction.at(places) ?? "0";
|
||||||
const roundingDigit = roundedFraction.at(CENTS_PRECISION) ?? "0";
|
|
||||||
|
|
||||||
const rounded = roundingDigit >= "5"
|
const rounded = (roundingDigit >= "5" ? BigInt(units) + 1n : BigInt(units))
|
||||||
? (BigInt(cents) + 1n).toString().padStart(cents.length, "0")
|
.toString()
|
||||||
: cents;
|
.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 берёт часовой пояс браузера пользователя.
|
// Без timeZone Intl берёт часовой пояс браузера пользователя.
|
||||||
|
|||||||
Reference in New Issue
Block a user