import { type HiddenAmount, type NumberFormatOptions, NumeralSystem, DecimalSeparator, DigitGroupingSymbol } from '@/core/numeral.ts'; import { DEFAULT_DECIMAL_NUMBER_COUNT, MAX_SUPPORTED_DECIMAL_NUMBER_COUNT, DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts'; import { isDefined, isString, isNumber, replaceAll, removeAll } from './common.ts'; export function sumAmounts(amounts: number[]): number { let sum = 0; for (const amount of amounts) { sum += amount; } return sum; } export function appendDigitGroupingSymbolAndDecimalSeparator(textualNumber: string, options: NumberFormatOptions): string { if (!textualNumber) { return textualNumber; } const numeralSystem = options.numeralSystem || NumeralSystem.Default; const digitGroupingType = options.digitGrouping; const digitGroupingSymbol = options.digitGroupingSymbol || DigitGroupingSymbol.Default.symbol; const decimalSeparator = options.decimalSeparator || DecimalSeparator.Default.symbol; const negative = textualNumber.charAt(0) === '-'; if (negative) { textualNumber = textualNumber.substring(1); } const integerChars: string[] = []; const decimalChars: string[] = []; let currentDecimalSeparator = ''; if (textualNumber === DISPLAY_HIDDEN_AMOUNT) { for (let i = 0; i < textualNumber.length - 2; i++) { integerChars.push(textualNumber.charAt(i)); } const decimalStartIndex = Math.max(0, textualNumber.length - 2); for (let i = decimalStartIndex; i < textualNumber.length; i++) { decimalChars.push(textualNumber.charAt(i)); } } else { for (let i = 0; i < textualNumber.length; i++) { const ch = textualNumber.charAt(i); if (!currentDecimalSeparator) { if (numeralSystem.isDigit(ch)) { integerChars.push(ch); } else { currentDecimalSeparator = ch; } } else { if (numeralSystem.isDigit(ch)) { decimalChars.push(ch); } else { throw new Error('Number \"' + textualNumber + '\" is not a valid textual number'); } } } } let integer = ''; if (digitGroupingType) { integer = digitGroupingType.format(integerChars, digitGroupingSymbol); } else { integer = integerChars.join(''); } const decimals = decimalChars.join(''); if (decimals) { textualNumber = `${integer}${decimalSeparator}${decimals}`; } else { textualNumber = integer; } if (negative) { textualNumber = `-${textualNumber}`; } return textualNumber; } export function parseAmount(str: string, options: NumberFormatOptions): number { if (!isString(str)) { return 0; } if (!str || str.length < 1) { return 0; } const negative = str.charAt(0) === '-'; if (negative) { str = str.substring(1); } if (!str || str.length < 1) { return 0; } const sign = negative ? -1 : 1; const numeralSystem = options.numeralSystem || NumeralSystem.Default; const decimalSeparator = options.decimalSeparator || DecimalSeparator.Default.symbol; const digitGroupingSymbol = options.digitGroupingSymbol || DigitGroupingSymbol.Default.symbol; if (str.indexOf(digitGroupingSymbol) >= 0) { str = removeAll(str, digitGroupingSymbol); } let decimalSeparatorPos = str.indexOf(decimalSeparator); if (decimalSeparatorPos < 0) { return sign * numeralSystem.parseInt(str) * 100; } else if (decimalSeparatorPos === 0) { str = numeralSystem.digitZero + str; decimalSeparatorPos++; } const integer = str.substring(0, decimalSeparatorPos); const decimals = str.substring(decimalSeparatorPos + 1, str.length); if (decimals.length < 1) { return sign * numeralSystem.parseInt(integer) * 100; } else if (decimals.length === 1) { return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals) * 10; } else if (decimals.length === 2) { return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals); } else { return sign * numeralSystem.parseInt(integer) * 100 + sign * numeralSystem.parseInt(decimals.substring(0, 2)); } } export function formatAmount(value: number, options: NumberFormatOptions): string { if (!Number.isSafeInteger(value)) { throw new Error('Number \"' + value + '\" is not amount number'); } const numeralSystem = options.numeralSystem || NumeralSystem.Default; let textualNumber = numeralSystem.formatNumber(value); if (!textualNumber) { return textualNumber; } const negative = textualNumber.charAt(0) === '-'; if (negative) { textualNumber = textualNumber.substring(1); } const digitGroupingType = options.digitGrouping; const digitGroupingSymbol = options.digitGroupingSymbol || DigitGroupingSymbol.Default.symbol; const decimalSeparator = options.decimalSeparator || DecimalSeparator.Default.symbol; let decimalNumberCount = options.decimalNumberCount; if (!isNumber(decimalNumberCount) || decimalNumberCount > MAX_SUPPORTED_DECIMAL_NUMBER_COUNT) { decimalNumberCount = DEFAULT_DECIMAL_NUMBER_COUNT; } let integer = numeralSystem.digitZero; let decimals = numeralSystem.doubleDigitZero; if (textualNumber.length > 2) { integer = textualNumber.substring(0, textualNumber.length - 2); decimals = textualNumber.substring(textualNumber.length - 2); } else if (textualNumber.length === 2) { decimals = textualNumber; } else if (textualNumber.length === 1) { decimals = numeralSystem.digitZero + textualNumber; } if (decimalNumberCount === 0) { if (decimals === numeralSystem.doubleDigitZero) { decimals = ''; } else if (decimals.charAt(1) === numeralSystem.digitZero) { decimals = decimals.charAt(0); } } else if (decimalNumberCount === 1) { if (decimals.charAt(1) === numeralSystem.digitZero) { decimals = decimals.charAt(0); } } if (options.trimTailZero) { if (decimals.charAt(0) === numeralSystem.digitZero && decimals.charAt(1) === numeralSystem.digitZero) { decimals = ''; } else if (decimals.charAt(0) !== numeralSystem.digitZero && decimals.charAt(1) === numeralSystem.digitZero) { decimals = decimals.charAt(0); } } if (integer && integer.length > 1 && digitGroupingType) { integer = digitGroupingType.format(integer.split(''), digitGroupingSymbol); } if (decimals) { textualNumber = `${integer}${decimalSeparator}${decimals}`; } else { textualNumber = integer; } if (negative) { textualNumber = `-${textualNumber}`; } return textualNumber; } export function formatHiddenAmount(value: HiddenAmount, options: NumberFormatOptions): string { return appendDigitGroupingSymbolAndDecimalSeparator(value, options); } export function formatNumber(value: number, options: NumberFormatOptions, precision?: number): string { const numeralSystem = options.numeralSystem || NumeralSystem.Default; if (isDefined(precision)) { const ratio = Math.pow(10, precision); const normalizedValue = Math.trunc(value * ratio); const textualValue = numeralSystem.formatNumber(normalizedValue / ratio); return appendDigitGroupingSymbolAndDecimalSeparator(textualValue, options); } else { const textualValue = numeralSystem.formatNumber(value); return appendDigitGroupingSymbolAndDecimalSeparator(textualValue, options); } } export function formatPercent(value: number, precision: number, lowPrecisionValue: string, options: NumberFormatOptions): string { const numeralSystem = options.numeralSystem || NumeralSystem.Default; const ratio = Math.pow(10, precision); const normalizedValue = Math.trunc(value * ratio); if (value > 0 && normalizedValue < 1 && lowPrecisionValue) { const systemDecimalSeparator = DecimalSeparator.Dot.symbol; const decimalSeparator = options.decimalSeparator || DecimalSeparator.Default.symbol; lowPrecisionValue = numeralSystem.replaceWesternArabicDigitsToLocalizedDigits(lowPrecisionValue); if (systemDecimalSeparator === decimalSeparator) { return lowPrecisionValue + '%'; } return replaceAll(lowPrecisionValue, systemDecimalSeparator, decimalSeparator) + '%'; } return formatNumber(value, options, precision) + '%'; } export function getAmountWithDecimalNumberCount(amount: number, decimalNumberCount: number): number { if (decimalNumberCount === 0) { return Math.trunc(amount / 100) * 100; } else if (decimalNumberCount === 1) { return Math.trunc(amount / 10) * 10; } return amount; } export function formatExchangeRateAmount(exchangeRateAmount: number, options: NumberFormatOptions): string { const numeralSystem = options.numeralSystem || NumeralSystem.Default; const rateStr = numeralSystem.formatNumber(exchangeRateAmount); const decimalSeparator = DecimalSeparator.Dot.symbol; if (rateStr.indexOf(decimalSeparator) < 0) { return appendDigitGroupingSymbolAndDecimalSeparator(rateStr, options); } else { let firstNonZeroPos = 0; for (let i = 0; i < rateStr.length; i++) { if (rateStr.charAt(i) !== decimalSeparator && rateStr.charAt(i) !== numeralSystem.digitZero) { firstNonZeroPos = Math.min(i + 4, rateStr.length); break; } } const trimmedRateStr = rateStr.substring(0, Math.max(6, Math.max(firstNonZeroPos, rateStr.indexOf(decimalSeparator) + 2))); return appendDigitGroupingSymbolAndDecimalSeparator(trimmedRateStr, options); } } export function getAdaptiveDisplayAmountRate(amount1: number, amount2: number, options: NumberFormatOptions, fromExchangeRate?: { rate: string }, toExchangeRate?: { rate: string }): string | null { const numeralSystem = options.numeralSystem || NumeralSystem.Default; if (!amount1 || !amount2 || amount1 === amount2) { if (!fromExchangeRate || !fromExchangeRate.rate || !toExchangeRate || !toExchangeRate.rate) { return null; } amount1 = parseFloat(fromExchangeRate.rate); amount2 = parseFloat(toExchangeRate.rate); } if (amount1 > amount2) { const rate = amount1 / amount2; const displayRateStr = formatExchangeRateAmount(rate, options); return `${displayRateStr} : ${numeralSystem.getLocalizedDigit(1)}`; } else { const rate = amount2 / amount1; const displayRateStr = formatExchangeRateAmount(rate, options); return `${numeralSystem.getLocalizedDigit(1)} : ${displayRateStr}`; } } export function getExchangedAmountByRate(amount: number, fromRate: string, toRate: string): number | null { const exchangeRate = parseFloat(toRate) / parseFloat(fromRate); if (!isNumber(exchangeRate)) { return null; } return amount * exchangeRate; }