import { Group, GROUP_DIGITS_INDEX, GROUP_PLACE_INDEX, GroupPlace, } from '../../common'; import { numberToExponential } from '../../exponent'; import { CENTILLIONS_PREFIXES, CentillionsPrefix, DECILLIONS_PREFIXES, DecillionsPrefix, DECIMAL_POINT, EMPTY_GROUP_DIGITS, EXPONENT_DELIMITER, GROUPING_SYMBOL, HUNDRED, ILLION_SUFFIX, MILLIA_PREFIX, MILLIONS_PREFIXES, MILLIONS_SPECIAL_PREFIXES, MillionsPrefix, MillionsSpecialPrefix, NEGATIVE, ONES, OnesName, SHORT_MILLIA_DELIMITER, TEN_PLUS_ONES, TenPlusOnesName, TENS, TENS_ONES_SEPARATOR, TensName, THOUSAND, } from './common'; /** * Builds a name for numbers in tens and ones. * @param tens - Tens digit. * @param ones - Ones digit. * @param addTensDashes - Whether to add dashes between the tens and ones. * @returns string The name for the number. */ const makeTensName = (tens: number, ones: number, addTensDashes = false) => { if (tens === 0) { return ONES[ones]; } if (tens === 1) { return TEN_PLUS_ONES[ones] as unknown as TenPlusOnesName; } if (ones === 0) { return TENS[tens]; } return `${TENS[tens] as Exclude}${addTensDashes ? TENS_ONES_SEPARATOR : ' '}${ONES[ones] as Exclude}` as const; }; /** * Builds a name for numbers in hundreds, tens, and ones. * @param hundreds - Hundreds digit. * @param tens - Tens digit. * @param ones - Ones digit. * @param addTensDashes - Whether to add dashes between the tens and ones. * @returns string The name for the number. */ const makeHundredsName = (hundreds: number, tens: number, ones: number, addTensDashes = false) => { if (hundreds === 0) { return makeTensName(tens, ones, addTensDashes); } if (tens === 0 && ones === 0) { return `${ONES[hundreds]} ${HUNDRED}` as const; } return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones, addTensDashes)}` as const; }; /** * Builds a name for numbers in the millions. * @param millions - Millions digit. * @param milliaCount - Number of millia- groups. * @returns string The millions prefix. */ const makeMillionsPrefix = (millions: number, milliaCount: GroupPlace) => { if (milliaCount > 0) { return MILLIONS_PREFIXES[millions] as MillionsPrefix; } return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix; }; /** * Builds a name for numbers in the decillions. * @param decillions - Decillions digit. * @param millions - Millions digit. * @param milliaCount - Number of millia- groups. * @returns string The decillions prefix. */ const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: GroupPlace) => { if (decillions === 0) { return makeMillionsPrefix(millions, milliaCount); } const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix; const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix; return `${onesPrefix}${tensName}` as const; }; /** * Builds a name for numbers in the centillions. * @param centillions - Centillions digit. * @param decillions - Decillions digit. * @param millions - Millions digit. * @param milliaCount - Number of millia- groups. * @returns string The centillions prefix. */ const makeCentillionsPrefix = ( centillions: number, decillions: number, millions: number, milliaCount: GroupPlace, ) => { if (centillions === 0) { return makeDecillionsPrefix(decillions, millions, milliaCount); } const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix; const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix; const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix; return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const; }; const repeatString = (s: string, count: GroupPlace) => { let result = ''; for (let i = BigInt(0); i < count; i += BigInt(1)) { result += s; } return result; }; const getGroupName = (place: GroupPlace, shortenMillia: boolean) => { if (place === BigInt(0)) { return '' as const; } if (place === BigInt(1)) { return THOUSAND; } const bigGroupPlace = place - BigInt(1); const groupGroups = bigGroupPlace .toString() .split('') .reduceRight( (acc, c, i, cc) => { const firstGroup = acc.at(0); const currentPlace = BigInt(Math.floor((cc.length - i - 1) / 3)); const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group; if (typeof firstGroup === 'undefined') { newGroup[GROUP_DIGITS_INDEX] = c; return [newGroup]; } if (firstGroup[0].length > 2) { newGroup[GROUP_DIGITS_INDEX] = c; return [newGroup, ...acc]; } newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0]; return [ newGroup, ...acc.slice(1), ]; }, [], ) .map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const) .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS) .map(([groupDigits, groupPlace]) => { const [hundreds, tens, ones] = groupDigits.split('').map(Number); if (groupPlace < 1) { return makeCentillionsPrefix(hundreds, tens, ones, groupPlace); } const milliaSuffix = ( shortenMillia && groupPlace > 1 ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}` : repeatString(MILLIA_PREFIX, groupPlace) ); if (groupDigits === '001') { return milliaSuffix; } return makeCentillionsPrefix(hundreds, tens, ones, groupPlace) + milliaSuffix; }) .join(''); if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) { return `${groupGroups}${ILLION_SUFFIX}` as const; } if (bigGroupPlace > 10) { return `${groupGroups}t${ILLION_SUFFIX}` as const; } return `${groupGroups}${ILLION_SUFFIX}` as const; }; export interface MakeGroupsOptions { addTensDashes?: boolean; shortenMillia?: boolean; } export const makeGroups = (groups: Group[], options?: MakeGroupsOptions): string[] => { const filteredGroups = groups.filter(([digits, place]) => ( place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS )); return filteredGroups.map( ([group, place]) => { const makeHundredsArgs = group .padStart(3, '0') .split('') .map((s) => Number(s)) as [number, number, number]; const groupDigitsName = makeHundredsName( ...makeHundredsArgs, options?.addTensDashes ?? false, ); const groupName = getGroupName(place, options?.shortenMillia ?? false); if (groupName.length > 0) { return `${groupDigitsName} ${groupName}`; } return groupDigitsName; }, ); }; /** * Group a number string into groups of three digits, starting from the decimal point. * @param value - The number string to group. */ export const group = (value: string): Group[] => { const [significand, exponentString] = numberToExponential( value, { decimalPoint: DECIMAL_POINT, groupingSymbol: GROUPING_SYMBOL, exponentDelimiter: EXPONENT_DELIMITER, }, ) .split(EXPONENT_DELIMITER); const exponent = Number(exponentString); const significantDigits = significand.replace(DECIMAL_POINT, ''); return significantDigits.split('').reduce( (acc, c, i) => { const currentPlace = BigInt(Math.floor((exponent - i) / 3)); const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace]; const currentPlaceInGroup = 2 - ((exponent - i) % 3); if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) { const lastGroupDigits = lastGroup[0].split(''); lastGroupDigits[currentPlaceInGroup] = c; return [...acc.slice(0, -1) ?? [], [ lastGroupDigits.join(''), currentPlace, ]]; } return [...acc, [c.padEnd(3, '0'), currentPlace]]; }, [], ); }; /** * Formats the final tokenized string. * @param tokens - The tokens to finalize. */ export const finalize = (tokens: string[]) => ( tokens .map((t) => t.trim()) .join(' ') .trim() ); /** * Makes a negative string. * @param s - The string to make negative. */ export const makeNegative = (s: string) => ( `${NEGATIVE} ${s}` );