/** * 与后端约定:金额存最小货币单位(如 NPR 2 位小数 → 分);展示时除以 10^displayDecimals。 * 展示分隔符与 {@link CurrencyFormatter} / 后台「货币格式」设置一致。 */ import { getCurrencyDisplayFormat } from "@/lib/currency-display-settings"; import { usePlayerSessionStore } from "@/stores/player-session-store"; const DEFAULT_DECIMAL_PLACES = 2; export function getCurrencyDecimalPlaces(currencyCode: string): number { const code = currencyCode.trim().toUpperCase(); const row = usePlayerSessionStore .getState() .currencies.find((item) => item.code === code); const decimals = row?.decimal_places; if (typeof decimals === "number" && Number.isFinite(decimals) && decimals >= 0) { return decimals; } return DEFAULT_DECIMAL_PLACES; } function formatIntegerWithThousands(value: number, thousandsSep: string): string { const digits = String(Math.trunc(value)); if (!thousandsSep) return digits; return digits.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep); } function formatMinorWithDisplaySettings( minorUnits: number, format = getCurrencyDisplayFormat(), ): string { const { displayDecimals, decimalSeparator, thousandsSeparator } = format; const divisor = Math.max(1, 10 ** displayDecimals); const negative = minorUnits < 0; const abs = Math.abs(minorUnits); const integerPart = Math.floor(abs / divisor); const fractionRaw = abs % divisor; const fractionPadded = String(fractionRaw).padStart(displayDecimals, "0"); const integerPartFormatted = formatIntegerWithThousands( integerPart, thousandsSeparator, ); if (displayDecimals === 0) { return `${negative ? "-" : ""}${integerPartFormatted}`; } return `${negative ? "-" : ""}${integerPartFormatted}${decimalSeparator}${fractionPadded}`; } export function formatMinorAmount( minor: number | string, decimalPlaces?: number, ): string { const n = typeof minor === "string" ? Number(minor) : minor; if (!Number.isFinite(n)) return "—"; const format = getCurrencyDisplayFormat(); const resolvedDisplayDecimals = typeof decimalPlaces === "number" && Number.isFinite(decimalPlaces) && decimalPlaces >= 0 ? Math.min(12, Math.trunc(decimalPlaces)) : format.displayDecimals; return formatMinorWithDisplaySettings(n, { ...format, displayDecimals: resolvedDisplayDecimals, }); } export function formatMinorAsCurrency( minor: number | string, currencyCode: string, decimalPlaces?: number, ): string { const amount = formatMinorAmount(minor, decimalPlaces); return `${currencyCode} ${amount}`; } /** * 用户输入如 `1000` 或 `1,000.50` → 最小货币单位整数(按币种实际 decimal_places)。 */ export function parseDecimalInputToMinor( raw: string, decimalPlacesOrCurrencyCode?: number | string, ): number | null { const decimalPlaces = typeof decimalPlacesOrCurrencyCode === "string" ? getCurrencyDecimalPlaces(decimalPlacesOrCurrencyCode) : typeof decimalPlacesOrCurrencyCode === "number" && Number.isFinite(decimalPlacesOrCurrencyCode) && decimalPlacesOrCurrencyCode >= 0 ? decimalPlacesOrCurrencyCode : decimalPlacesOrCurrencyCode; const resolvedDecimalPlaces = decimalPlaces ?? DEFAULT_DECIMAL_PLACES; const format = getCurrencyDisplayFormat(); let cleaned = raw.trim(); if (format.thousandsSeparator) { cleaned = cleaned.split(format.thousandsSeparator).join(""); } if (format.decimalSeparator && format.decimalSeparator !== ".") { cleaned = cleaned.replaceAll(format.decimalSeparator, "."); } cleaned = cleaned.replace(/,/g, ""); if (cleaned === "") return null; const n = Number(cleaned); if (!Number.isFinite(n) || n < 0) return null; const factor = 10 ** resolvedDecimalPlaces; const minor = Math.round(n * factor); if (!Number.isSafeInteger(minor)) return null; return minor; }