import { Group, GROUP_DIGITS_INDEX, GROUP_PLACE_INDEX, GroupDigits, GroupPlace, } from '../../../common'; import { numberToExponential } from '../../../exponent'; import { CENTILLIONS_PREFIXES, CentillionsPrefix, DECILLIONS_PREFIXES, DecillionsPrefix, DECIMAL_POINT, EMPTY_GROUP_DIGITS, EXPONENT_DELIMITER, GROUP_SEPARATOR, GROUPING_SYMBOL, HUNDRED, ILLION_SUFFIX, MILLIA_PREFIX, MILLIONS_PREFIXES, MILLIONS_SPECIAL_PREFIXES, MillionsPrefix, MillionsSpecialPrefix, NEGATIVE, ONES, OnesName, SHORT_MILLIA_DELIMITER, SHORT_MILLIA_ILLION_DELIMITER, T_AFFIX, TEN_PLUS_ONES, TenPlusOnesName, TENS, TENS_ONES_SEPARATOR, TensName, THOUSAND, } from '../../en/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: boolean) => { 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: boolean) => { 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 currentMillia - Current millia- group. * @param longestMilliaCount - Number of millia- groups. * @returns string The millions prefix. */ const makeMillionsPrefix = ( millions: number, currentMillia: GroupPlace, longestMilliaCount: GroupPlace, ) => { if (currentMillia === BigInt(0) && longestMilliaCount === BigInt(0)) { return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix; } return MILLIONS_PREFIXES[millions] as MillionsPrefix; }; /** * Builds a name for numbers in the decillions. * @param decillions - Decillions digit. * @param millions - Millions digit. * @param currentMillia - Current millia- group. * @param longestMilliaCount - Number of millia- groups. * @returns string The decillions prefix. */ const makeDecillionsPrefix = ( decillions: number, millions: number, currentMillia: GroupPlace, longestMilliaCount: GroupPlace, ) => { if (decillions === 0) { return makeMillionsPrefix(millions, currentMillia, longestMilliaCount); } 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 currentMillia - Current millia- group. * @param longestMilliaCount - Number of millia- groups. * @returns string The centillions prefix. */ const makeCentillionsPrefix = ( centillions: number, decillions: number, millions: number, currentMillia: GroupPlace, longestMilliaCount: GroupPlace, ) => { if (centillions === 0) { return makeDecillionsPrefix(decillions, millions, currentMillia, longestMilliaCount); } 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; }; /** * Repeats a string a given number of times. * @param s - String to repeat. * @param count - Number of times to repeat the string. * @returns string The repeated string. */ 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 as GroupDigits; return [newGroup]; } if (firstGroup[0].length > 2) { newGroup[GROUP_DIGITS_INDEX] = c as GroupDigits; return [newGroup, ...acc]; } newGroup[GROUP_DIGITS_INDEX] = (c + firstGroup[0]) as GroupDigits; return [ newGroup, ...acc.slice(1), ]; }, [], ) .map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const) .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS) .map(([groupDigits, groupPlace], _index, millias) => { const [hundreds, tens, ones] = groupDigits.split('').map(Number); const largestMillia = millias[0][GROUP_PLACE_INDEX]; const centillionsPrefix = makeCentillionsPrefix( hundreds, tens, ones, groupPlace, largestMillia, ); if (groupPlace < 1) { return centillionsPrefix; } const milliaSuffix = ( shortenMillia && groupPlace > 1 ? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}${SHORT_MILLIA_ILLION_DELIMITER}` : repeatString(MILLIA_PREFIX, groupPlace) ); if (groupDigits === '001' && groupPlace === largestMillia) { return milliaSuffix; } return `${centillionsPrefix}${milliaSuffix}`; }) .join(''); if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) { return `${groupGroups}${ILLION_SUFFIX}` as const; } if (bigGroupPlace > 10) { // vigin - t - illion, cen - t - illion, etc. return `${groupGroups}${T_AFFIX}${ILLION_SUFFIX}` as const; } return `${groupGroups}${ILLION_SUFFIX}` as const; }; export interface StringifyGroupsOptions { /** * Whether to add dashes between tens and ones (e.g. "sixty-nine"). */ addTensDashes?: boolean; /** * Use "millia^2-tillion" instead of "milliamilliatillion". */ shortenMillia?: boolean; } /** * Creates a group string. * @param groups - The groups. * @param options - Options to use when creating the group. * @returns string[] The groups represented into strings. */ export const stringifyGroups = (groups: Group[], options?: StringifyGroupsOptions): 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 ?? true, ); 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. * @returns Group[] The groups. */ export const splitIntoGroups = (value: string): Group[] => { const [significand, exponentString] = numberToExponential( value, { decimalPoint: DECIMAL_POINT, groupingSymbol: GROUPING_SYMBOL, exponentDelimiter: EXPONENT_DELIMITER, }, ) .split(EXPONENT_DELIMITER); // FIXME use bigint for exponent and indexing??? const exponent = Number(exponentString); const significantDigits = significand.replace(new RegExp(`\\${DECIMAL_POINT}`, 'g'), ''); 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('') as GroupDigits, currentPlace, ]]; } return [...acc, [c.padEnd(3, '0') as GroupDigits, currentPlace]]; }, [], ); }; export interface MergeTokensOptions { oneGroupPerLine?: boolean; } /** * Formats the final tokenized string. * @param tokens - The tokens to finalize. * @param options - The options to use. */ export const mergeTokens = (tokens: string[], options?: MergeTokensOptions) => ( tokens .map((t) => t.trim()) .join(options?.oneGroupPerLine ? '\n' : GROUP_SEPARATOR) .trim() ); /** * Makes a negative string. * @param s - The string to make negative. */ export const makeNegative = (s: string) => { const negativePrefix = `${NEGATIVE} `; return s.startsWith(negativePrefix) ? s.slice(negativePrefix.length) : `${negativePrefix}${s}`; };