|
- // noinspection SpellCheckingInspection
-
- import { Group } from '../common';
- import { numberToExponential } from '../exponent';
-
- const DECIMAL_POINT = '.';
-
- const GROUPING_SYMBOL = ',';
-
- const NEGATIVE = 'negative';
-
- const ONES = [
- 'zero',
- 'one',
- 'two',
- 'three',
- 'four',
- 'five',
- 'six',
- 'seven',
- 'eight',
- 'nine',
- ] as const;
-
- type OnesName = typeof ONES[number];
-
- const TEN_PLUS_ONES = [
- 'ten',
- 'eleven',
- 'twelve',
- 'thirteen',
- 'fourteen',
- 'fifteen',
- 'sixteen',
- 'seventeen',
- 'eighteen',
- 'nineteen',
- ] as const;
-
- type TenPlusOnesName = typeof TEN_PLUS_ONES[number];
-
- const TENS = [
- 'zero',
- TEN_PLUS_ONES[0],
- 'twenty',
- 'thirty',
- 'forty',
- 'fifty',
- 'sixty',
- 'seventy',
- 'eighty',
- 'ninety',
- ] as const;
-
- type TensName = typeof TENS[number];
-
- const HUNDRED = 'hundred' as const;
-
- const THOUSAND = 'thousand' as const;
-
- // const ILLION_ORDINAL_SUFFIX = 'illionth' as const;
-
- // const THOUSAND_ORDINAL = 'thousandth' as const;
-
- const MILLIONS_SPECIAL_PREFIXES = [
- '',
- 'm',
- 'b',
- 'tr',
- 'quadr',
- 'quint',
- 'sext',
- 'sept',
- 'oct',
- 'non',
- ] as const;
-
- type MillionsSpecialPrefix = typeof MILLIONS_SPECIAL_PREFIXES[number];
-
- const MILLIONS_PREFIXES = [
- '',
- 'un',
- 'duo',
- 'tre',
- 'quattuor',
- 'quin',
- 'sex',
- 'septen',
- 'octo',
- 'novem',
- ] as const;
-
- type MillionsPrefix = typeof MILLIONS_PREFIXES[number];
-
- const DECILLIONS_PREFIXES = [
- '',
- 'dec',
- 'vigin',
- 'trigin',
- 'quadragin',
- 'quinquagin',
- 'sexagin',
- 'septuagin',
- 'octogin',
- 'nonagin',
- ] as const;
-
- type DecillionsPrefix = typeof DECILLIONS_PREFIXES[number];
-
- const CENTILLIONS_PREFIXES = [
- '',
- 'cen',
- 'duocen',
- 'trecen',
- 'quadringen',
- 'quingen',
- 'sescen',
- 'septingen',
- 'octingen',
- 'nongen',
- ] as const;
-
- type CentillionsPrefix = typeof CENTILLIONS_PREFIXES[number];
-
- const MILLIA_PREFIX = 'millia' as const;
-
- const ILLION_SUFFIX = 'illion' as const;
-
- const makeTensName = (tens: number, ones: number) => {
- if (tens === 0) {
- return ONES[ones];
- }
-
- if (tens === 1) {
- return TEN_PLUS_ONES[ones] as TenPlusOnesName;
- }
-
- if (ones === 0) {
- return TENS[tens];
- }
-
- return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>} ${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
- };
-
- 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;
- };
-
- const makeMillionsPrefix = (millions: number, milliaCount: number) => {
- if (milliaCount > 0) {
- return MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
- }
-
- return MILLIONS_SPECIAL_PREFIXES[millions] as Exclude<MillionsSpecialPrefix, ''>;
- };
-
- const makeDecillionsPrefix = (decillions: number, millions: number, milliaCount: number) => {
- if (decillions === 0) {
- return makeMillionsPrefix(millions, milliaCount);
- }
-
- const onesPrefix = MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
- const tensName = DECILLIONS_PREFIXES[decillions] as Exclude<DecillionsPrefix, ''>;
- return `${onesPrefix}${tensName}` as const;
- };
-
- const makeCentillionsPrefix = (centillions: number, decillions: number, millions: number, milliaCount: number) => {
- if (centillions === 0) {
- return makeDecillionsPrefix(decillions, millions, milliaCount);
- }
-
- const onesPrefix = MILLIONS_PREFIXES[millions] as Exclude<MillionsPrefix, ''>;
- const tensName = DECILLIONS_PREFIXES[decillions] as Exclude<DecillionsPrefix, ''>;
- const hundredsName = CENTILLIONS_PREFIXES[centillions] as Exclude<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(([group, groupPlace]) => [group.padStart(3, '0'), groupPlace] as const)
- .filter(([group]) => group !== '000')
- .map(([group, groupPlace]) => {
- const [hundreds, tens, ones] = group.split('').map(Number);
- if (groupPlace < 1) {
- return makeCentillionsPrefix(hundreds, tens, ones, groupPlace);
- }
-
- const milliaSuffix = (
- shortenMillia && groupPlace > 1
- ? `${MILLIA_PREFIX}^${groupPlace}`
- : MILLIA_PREFIX.repeat(groupPlace)
- );
-
- if (group === '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 exponentDelimiter = 'e';
- const [significand, exponentString] = numberToExponential(
- value,
- {
- decimalPoint: DECIMAL_POINT,
- groupingSymbol: GROUPING_SYMBOL,
- exponentDelimiter,
- })
- .split(exponentDelimiter);
- 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) ?? ['000', 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 = (s: string) => (
- s.split(' ').filter((s) => s.length > 0)
- );
-
- const FINAL_TOKEN = '';
-
- const getGroupFromGroupName = (groupName: string) => {
- if (groupName === THOUSAND) {
- return 1;
- }
-
- const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
- const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => {
- return groupNameBase === p;
- });
-
- if (specialMillions > -1) {
- return 1 + specialMillions;
- }
-
- let groupNameCurrent = groupNameBase;
-
- let millias = [0];
- let milliaIndex = 0;
-
- while (groupNameCurrent.length > 0) {
- if (groupNameCurrent === 't') {
- break;
- }
-
- const centillions = CENTILLIONS_PREFIXES.findIndex((p) => {
- return p.length > 0 && groupNameCurrent.startsWith(p);
- });
-
- if (centillions > -1) {
- milliaIndex = 0;
- millias[milliaIndex] += (centillions * 100);
- groupNameCurrent = groupNameCurrent.slice(CENTILLIONS_PREFIXES[centillions].length);
- continue;
- }
-
- const decillions = DECILLIONS_PREFIXES.findIndex((p) => {
- return p.length > 0 && groupNameCurrent.startsWith(p);
- });
-
- if (decillions > -1) {
- milliaIndex = 0;
- millias[milliaIndex] += decillions * 10;
- groupNameCurrent = groupNameCurrent.slice(DECILLIONS_PREFIXES[decillions].length);
- continue;
- }
-
- const millions = MILLIONS_PREFIXES.findIndex((p) => {
- return p.length > 0 && groupNameCurrent.startsWith(p);
- });
-
- if (millions > -1) {
- milliaIndex = 0;
- millias[milliaIndex] += millions;
- groupNameCurrent = groupNameCurrent.slice(MILLIONS_PREFIXES[millions].length);
- continue;
- }
-
- if (groupNameCurrent.startsWith(`${MILLIA_PREFIX}^`)) {
- // short millia
- groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
- const matchedMilliaArray = groupNameCurrent.match(/^\d+/);
- if (!matchedMilliaArray) {
- throw new Error(`Invalid groupName: ${groupName}`);
- }
- const matchedMillia = matchedMilliaArray[0];
- millias[Number(matchedMillia)] = millias[milliaIndex] || 1;
- millias[milliaIndex] = 0;
- groupNameCurrent = groupNameCurrent.slice(matchedMillia.length);
- }
-
- if (groupNameCurrent.startsWith(MILLIA_PREFIX)) {
- millias[milliaIndex + 1] = millias[milliaIndex] || 1;
- millias[milliaIndex] = 0;
- milliaIndex += 1;
- groupNameCurrent = groupNameCurrent.slice(MILLIA_PREFIX.length);
- continue;
- }
-
- break;
- }
-
- const bigGroupPlace = Number(
- millias
- .map((s) => s.toString().padStart(3, '0'))
- .reverse()
- .join('')
- );
-
- return 1 + bigGroupPlace;
- };
-
- enum ParseGroupsMode {
- INITIAL = 'unknown',
- ONES = 'ones',
- TENS = 'tens',
- TEN_PLUS_ONES = 'tenPlusOnes',
- HUNDRED = 'hundred',
- THOUSAND = '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) ?? ['000', 0];
-
- if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
- if (acc.mode === ParseGroupsMode.ONES) {
- const ones = ONES.findIndex((o) => o === acc.lastToken);
- lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
- }
-
- lastGroup[1] = getGroupFromGroupName(token);
-
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- lastToken: token,
- mode: ParseGroupsMode.THOUSAND,
- }
- }
-
- if (token === HUNDRED) {
- if (acc.mode === ParseGroupsMode.ONES) {
- 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,
- };
- }
- }
-
- if (token === FINAL_TOKEN) {
- if (acc.mode === ParseGroupsMode.ONES) {
- 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) {
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.ONES,
- groups: [...acc.groups, ['000', 0]],
- }
- }
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.ONES,
- }
- }
-
- const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
- if (tenPlusOnes > -1) {
- lastGroup[0] = `${lastGroup[0].slice(0, 1)}1${tenPlusOnes}`;
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TEN_PLUS_ONES,
- 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,
- 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(
- (s, group) => {
- const [groupDigits] = group;
- return `${s}${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 exponent = exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`;
- const significandInteger = digits.slice(0, 1);
- const significandFraction = digits.slice(1);
- if (significandFraction.length > 0) {
- return `${significandInteger}${DECIMAL_POINT}${significandFraction}e${exponent}`;
- }
- return `${significandInteger}e${exponent}`;
- };
|