121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
/**
|
||
* 与后端约定:金额存最小货币单位(如 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;
|
||
}
|