const DECIMAL_PART_COUNT = 2; const CENTS_PRECISION = 2; type FormatMoneyOptions = { withRoundedCents?: boolean; withSign?: boolean; withZeroCents?: boolean; }; const portfolioCategoryPresentation = { BOND: { color: "grape.6", label: "portfolio.bonds" }, CASH: { color: "green.6", label: "portfolio.structure.cash" }, OTHER: { color: "orange.6", label: "portfolio.structure.other" }, STOCK: { color: "blue.6", label: "portfolio.stocks" }, } as const; type Money = { currency: string; value: string }; const dateFormatter = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", timeZone: "UTC", year: "numeric", }); function formatDate(value: string): string { return dateFormatter.format(new Date(value)); } function formatDecimal(value: string): string { const [integer = "", fraction] = value.split(".", DECIMAL_PART_COUNT); const sign = integer.startsWith("-") ? "-" : ""; const digits = sign ? integer.slice(1) : integer; const groupSize = 3; const groups: string[] = []; for (let end = digits.length; end > 0; end -= groupSize) { groups.push(digits.slice(Math.max(0, end - groupSize), end)); } const grouped = groups.toReversed().join(" "); const suffix = fraction ? `,${fraction}` : ""; return `${sign}${grouped}${suffix}`; } function formatMoney( money: Money | null | undefined, { withRoundedCents = false, withSign = false, withZeroCents = false, }: FormatMoneyOptions = {}, ): string { if (!money) return "—"; const value = withRoundedCents ? roundDecimalToCents(money.value) : money.value; const [integerPart = "", fractionPart = ""] = value.split( ".", DECIMAL_PART_COUNT, ); const normalizedFraction = withRoundedCents || withZeroCents ? fractionPart.padEnd(CENTS_PRECISION, "0") : fractionPart; const decimal = normalizedFraction ? `${integerPart}.${normalizedFraction}` : integerPart; const currency = money.currency === "RUB" ? "₽" : money.currency; const formatted = `${formatDecimal(decimal)} ${currency}`; if (!withSign) return formatted; if (isZeroDecimal(value)) return value.startsWith("-") ? formatted.slice(1) : formatted; return value.startsWith("-") ? `−${formatted.slice(1)}` : `+${formatted}`; } function isZeroDecimal(value: string) { const unsigned = value.startsWith("-") ? value.slice(1) : value; const [integer = "", fraction = ""] = unsigned.split( ".", DECIMAL_PART_COUNT, ); return integer.replaceAll("0", "") === "" && fraction.replaceAll("0", "") === ""; } function roundDecimalToCents(value: string): 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 rounded = roundingDigit >= "5" ? (BigInt(cents) + 1n).toString().padStart(cents.length, "0") : cents; const roundedInteger = rounded.slice(0, -CENTS_PRECISION) || "0"; return `${sign}${roundedInteger}.${rounded.slice(-CENTS_PRECISION)}`; } // Без timeZone Intl берёт часовой пояс браузера пользователя. const snapshotFormatter = new Intl.DateTimeFormat("ru-RU", { dateStyle: "short", timeStyle: "short", }); function formatSnapshotTime(value: string): string { return snapshotFormatter.format(new Date(value)); } const logoSize = 160; const chartFormatter = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 4 }); function formatChartValue(value: number) { return chartFormatter.format(value); } function instrumentLogoUrl(logoName?: null | string): null | string { const stem = logoName?.trim().replace(/\.png$/i, ""); return stem ? `https://invest-brands.cdn-tinkoff.ru/${stem}x${logoSize}.png` : null; } export { formatChartValue, formatDate, formatDecimal, formatMoney, formatSnapshotTime, instrumentLogoUrl, portfolioCategoryPresentation, };