- 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, 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 '../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<TensName, 'zero' | 'ten'>}${addTensDashes ? TENS_ONES_SEPARATOR : ' '}${ONES[ones] as Exclude<OnesName, 'zero'>}` 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 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 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 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<Group[]>(
- (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], _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);
- const exponent = Number(exponentString);
- const significantDigits = significand.replace(new RegExp(`\\${DECIMAL_POINT}`, 'g'), '');
- return significantDigits.split('').reduce<Group[]>(
- (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]];
- },
- [],
- );
- };
-
- 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}`;
- };
|