|
- // noinspection SpellCheckingInspection
-
- import { Group, InvalidTokenError } from '../common';
- import { numberToExponential } from '../exponent';
-
- const DECIMAL_POINT = '.' as const;
-
- const GROUPING_SYMBOL = ',' as const;
-
- const NEGATIVE = 'negative' as const;
-
- const NEGATIVE_SYMBOL = '-' as const;
-
- const POSITIVE_SYMBOL = '+' as const;
-
- const SHORT_MILLIA_DELIMITER = '^' as const;
-
- const EXPONENT_DELIMITER = 'e' as const;
-
- const EMPTY_GROUP_DIGITS = '000' as const;
-
- const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, 0];
-
- /**
- * Ones number names.
- */
- const ONES = [
- 'zero',
- 'one',
- 'two',
- 'three',
- 'four',
- 'five',
- 'six',
- 'seven',
- 'eight',
- 'nine',
- ] as const;
-
- type OnesName = typeof ONES[number];
-
- /**
- * Ten plus ones number names.
- */
- const TEN_PLUS_ONES = [
- 'ten',
- 'eleven',
- 'twelve',
- 'thirteen',
- 'fourteen',
- 'fifteen',
- 'sixteen',
- 'seventeen',
- 'eighteen',
- 'nineteen',
- ] as const;
-
- type TenPlusOnesName = typeof TEN_PLUS_ONES[number];
-
- /**
- * Tens number names.
- */
- const TENS = [
- 'zero',
- TEN_PLUS_ONES[0],
- 'twenty',
- 'thirty',
- 'forty',
- 'fifty',
- 'sixty',
- 'seventy',
- 'eighty',
- 'ninety',
- ] as const;
-
- type TensName = typeof TENS[number];
-
- /**
- * Hundreds name.
- */
- const HUNDRED = 'hundred' as const;
-
- /**
- * Thousands name.
- */
- const THOUSAND = 'thousand' as const;
-
- // const ILLION_ORDINAL_SUFFIX = 'illionth' as const;
-
- // const THOUSAND_ORDINAL = 'thousandth' as const;
-
- /**
- * Special millions name.
- */
- const MILLIONS_SPECIAL_PREFIXES = [
- '',
- 'm',
- 'b',
- 'tr',
- 'quadr',
- 'quint',
- 'sext',
- 'sept',
- 'oct',
- 'non',
- ] as const;
-
- type MillionsSpecialPrefix = Exclude<typeof MILLIONS_SPECIAL_PREFIXES[number], ''>;
-
- /**
- * Millions name.
- */
- const MILLIONS_PREFIXES = [
- '',
- 'un',
- 'duo',
- 'tre',
- 'quattuor',
- 'quin',
- 'sex',
- 'septen',
- 'octo',
- 'novem',
- ] as const;
-
- type MillionsPrefix = Exclude<typeof MILLIONS_PREFIXES[number], ''>;
-
- /**
- * Decillions name.
- */
- const DECILLIONS_PREFIXES = [
- '',
- 'dec',
- 'vigin',
- 'trigin',
- 'quadragin',
- 'quinquagin',
- 'sexagin',
- 'septuagin',
- 'octogin',
- 'nonagin',
- ] as const;
-
- type DecillionsPrefix = Exclude<typeof DECILLIONS_PREFIXES[number], ''>;
-
- /**
- * Centillions name.
- */
- const CENTILLIONS_PREFIXES = [
- '',
- 'cen',
- 'duocen',
- 'trecen',
- 'quadringen',
- 'quingen',
- 'sescen',
- 'septingen',
- 'octingen',
- 'nongen',
- ] as const;
-
- type CentillionsPrefix = Exclude<typeof CENTILLIONS_PREFIXES[number], ''>;
-
- /**
- * Prefix for millia- number names.
- */
- const MILLIA_PREFIX = 'millia' as const;
-
- /**
- * Suffix for -illion number names.
- */
- const ILLION_SUFFIX = 'illion' as const;
-
- /**
- * Builds a name for numbers in tens and ones.
- * @param tens - Tens digit.
- * @param ones - Ones digit.
- * @returns The name for the number.
- */
- const makeTensName = (tens: number, ones: number) => {
- 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'>} ${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.
- * @returns The name for the number.
- */
- const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
- if (hundreds === 0) {
- return makeTensName(tens, ones);
- }
-
- if (tens === 0 && ones === 0) {
- return `${ONES[hundreds]} ${HUNDRED}` as const;
- }
-
- return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones)}` as const;
- };
-
- /**
- * Builds a name for numbers in the millions.
- * @param millions - Millions digit.
- * @param milliaCount - Number of millia- groups.
- * @returns The millions prefix.
- */
- const makeMillionsPrefix = (millions: number, milliaCount: number) => {
- 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 The decillions prefix.
- */
- const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => {
- 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 The centillions prefix.
- */
- const makeCentillionsPrefix = (
- centillions: number,
- decillions: number,
- millions: number,
- milliaCount: number,
- ) => {
- 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 getGroupName = (place: number, shortenMillia: boolean) => {
- if (place === 0) {
- return '' as const;
- }
-
- if (place === 1) {
- return THOUSAND;
- }
-
- const bigGroupPlace = place - 1;
- const groupGroups = bigGroupPlace
- .toString()
- .split('')
- .reduceRight<Group[]>(
- (acc, c, i, cc) => {
- const firstGroup = acc.at(0);
- const currentPlace = Math.floor((cc.length - i - 1) / 3);
- if (typeof firstGroup === 'undefined') {
- return [[c, currentPlace]];
- }
-
- if (firstGroup[0].length > 2) {
- return [[c, currentPlace], ...acc];
- }
-
- return [
- [c + firstGroup[0], currentPlace],
- ...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}`
- : MILLIA_PREFIX.repeat(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 const makeGroup = (
- group: string,
- place: number,
- options?: Record<string, unknown>,
- ): string => {
- const makeHundredsArgs = group
- .padStart(3, '0')
- .split('')
- .map((s) => Number(s)) as [number, number, number];
-
- const groupDigitsName = makeHundredsName(...makeHundredsArgs);
- const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false);
- if (groupName.length > 0) {
- return `${groupDigitsName} ${groupName}` as const;
- }
- 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<Group[]>(
- (acc, c, i) => {
- const currentPlace = Math.floor((exponent - i) / 3);
- const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
- const currentPlaceInGroup = 2 - ((exponent - i) % 3);
- if (lastGroup[1] === 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}`
- );
-
- export const tokenize = (stringValue: string) => (
- stringValue.split(' ').filter((maybeToken) => maybeToken.length > 0)
- );
-
- const FINAL_TOKEN = '' as const;
-
- interface DoParseState {
- groupNameCurrent: string;
- millias: number[];
- milliaIndex: number;
- done: boolean;
- }
-
- const doParseGroupName = (result: DoParseState): DoParseState => {
- if (result.groupNameCurrent.length < 1) {
- return {
- ...result,
- done: true,
- };
- }
-
- if (result.groupNameCurrent === 't') {
- // If the current group name is "t", then we're done.
- // We use the -t- affix to attach the group prefix to the -illion suffix, except for decillion.
- return {
- ...result,
- done: true,
- };
- }
-
- const centillions = CENTILLIONS_PREFIXES.findIndex((p) => (
- p.length > 0 && result.groupNameCurrent.startsWith(p)
- ));
- if (centillions > -1) {
- return {
- milliaIndex: 0,
- millias: result.millias.map((m, i) => (
- i === 0
- ? m + (centillions * 100)
- : m
- )),
- groupNameCurrent: result.groupNameCurrent.slice(
- CENTILLIONS_PREFIXES[centillions].length,
- ),
- done: false,
- };
- }
-
- const decillions = DECILLIONS_PREFIXES.findIndex((p) => (
- p.length > 0 && result.groupNameCurrent.startsWith(p)
- ));
- if (decillions > -1) {
- return {
- milliaIndex: 0,
- millias: result.millias.map((m, i) => (
- i === 0
- ? m + (decillions * 10)
- : m
- )),
- groupNameCurrent: result.groupNameCurrent.slice(
- DECILLIONS_PREFIXES[decillions].length,
- ),
- done: false,
- };
- }
-
- const millions = MILLIONS_PREFIXES.findIndex((p) => (
- p.length > 0 && result.groupNameCurrent.startsWith(p)
- ));
- if (millions > -1) {
- return {
- milliaIndex: 0,
- millias: result.millias.map((m, i) => (
- i === 0
- ? m + millions
- : m
- )),
- groupNameCurrent: result.groupNameCurrent.slice(
- MILLIONS_PREFIXES[millions].length,
- ),
- done: false,
- };
- }
-
- if (result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`)) {
- // short millia
- const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/);
- if (!matchedMilliaArray) {
- throw new InvalidTokenError(result.groupNameCurrent);
- }
- const matchedMillia = matchedMilliaArray[0];
- const newMillia = Number(matchedMillia);
- const oldMillia = result.milliaIndex;
- const newMillias = [...result.millias];
- newMillias[newMillia] = newMillias[oldMillia] || 1;
- newMillias[oldMillia] = 0;
- return {
- milliaIndex: newMillia,
- millias: newMillias,
- groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
- done: false,
- };
- }
-
- if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) {
- const newMillia = result.milliaIndex + 1;
- const oldMillia = result.milliaIndex;
- const newMillias = [...result.millias];
- newMillias[newMillia] = newMillias[oldMillia] || 1;
- newMillias[oldMillia] = 0;
- return {
- milliaIndex: newMillia,
- millias: newMillias,
- groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
- done: false,
- };
- }
-
- throw new InvalidTokenError(result.groupNameCurrent);
- };
-
- const getGroupPlaceFromGroupName = (groupName: string) => {
- if (groupName === THOUSAND) {
- return 1;
- }
-
- const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
- const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);
-
- if (specialMillions > -1) {
- return specialMillions + 1;
- }
-
- let result: DoParseState = {
- groupNameCurrent: groupNameBase,
- millias: [0],
- milliaIndex: 0,
- done: false,
- };
-
- do {
- result = doParseGroupName(result);
- } while (!result.done);
-
- const bigGroupPlace = Number(
- result.millias
- .map((s) => s.toString().padStart(3, '0'))
- .reverse()
- .join(''),
- );
-
- return bigGroupPlace + 1;
- };
-
- enum ParseGroupsMode {
- INITIAL = 'unknown',
- ONES_MODE = 'ones',
- TENS_MODE = 'tens',
- TEN_PLUS_ONES_MODE = 'tenPlusOnes',
- HUNDRED_MODE = 'hundred',
- THOUSAND_MODE = 'thousand',
- DONE = 'done',
- }
-
- interface ParserState {
- lastToken?: string;
- groups: Group[];
- mode: ParseGroupsMode;
- }
-
- export const parseGroups = (tokens: string[]) => {
- const { groups } = [...tokens, FINAL_TOKEN].reduce<ParserState>(
- (acc, token) => {
- const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
-
- if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
- if (acc.mode === ParseGroupsMode.ONES_MODE) {
- const ones = ONES.findIndex((o) => o === acc.lastToken);
- if (ones > -1) {
- lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
- }
- }
-
- lastGroup[1] = getGroupPlaceFromGroupName(token);
-
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- lastToken: token,
- mode: ParseGroupsMode.THOUSAND_MODE,
- };
- }
-
- if (token === HUNDRED) {
- if (acc.mode === ParseGroupsMode.ONES_MODE) {
- const hundreds = ONES.findIndex((o) => o === acc.lastToken);
- lastGroup[0] = `${hundreds}${lastGroup[0].slice(1)}`;
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- mode: ParseGroupsMode.HUNDRED_MODE,
- };
- }
- }
-
- if (token === FINAL_TOKEN) {
- if (acc.mode === ParseGroupsMode.ONES_MODE) {
- const ones = ONES.findIndex((o) => o === acc.lastToken);
- lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
- lastGroup[1] = 0;
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- mode: ParseGroupsMode.DONE,
- };
- }
- }
-
- if (ONES.includes(token as OnesName)) {
- if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.ONES_MODE,
- groups: [...acc.groups, [...EMPTY_PLACE]],
- };
- }
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.ONES_MODE,
- };
- }
-
- const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
- if (tenPlusOnes > -1) {
- if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.ONES_MODE,
- groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[1] - 1]],
- };
- }
-
- lastGroup[0] = `${lastGroup[0].slice(0, 1)}1${tenPlusOnes}`;
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- };
- }
-
- const tens = TENS.findIndex((t) => t === token);
- if (tens > -1) {
- lastGroup[0] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[0].slice(2)}`;
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TENS_MODE,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- };
- }
-
- return {
- ...acc,
- lastToken: token,
- };
- },
- {
- lastToken: undefined,
- groups: [],
- mode: ParseGroupsMode.INITIAL,
- },
- );
-
- return groups;
- };
-
- export const combineGroups = (groups: Group[]) => {
- const groupsSorted = groups.sort((a, b) => b[1] - a[1]); // sort by place
- const firstGroup = groupsSorted[0];
- const firstGroupPlace = firstGroup[1];
- const digits = groupsSorted.reduce(
- (previousDigits, thisGroup) => {
- const [groupDigits] = thisGroup;
- return `${previousDigits}${groupDigits}`;
- },
- '',
- ).replace(/^0+/, '') || '0';
- const firstGroupDigits = firstGroup[0];
- const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, '');
- const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length;
- const exponentValue = BigInt((firstGroupPlace * 3) + (2 - exponentExtra));
- const isExponentNegative = exponentValue < 0;
- const exponentValueAbs = isExponentNegative ? -exponentValue : exponentValue;
- const exponentSign = isExponentNegative ? NEGATIVE_SYMBOL : POSITIVE_SYMBOL;
- const exponent = `${exponentSign}${exponentValueAbs}`;
- const significandInteger = digits.slice(0, 1);
- const significandFraction = digits.slice(1).replace(/0+$/, '');
- if (significandFraction.length > 0) {
- return `${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
- }
- return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
- };
|