From 83f0f280124f672b9f242378799b5d0f1b579e14 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Tue, 15 Aug 2023 12:02:40 +0800 Subject: [PATCH] Split system functions Put functions with different purposes into their own files. --- packages/core/src/systems/en-US/common.ts | 172 +++++++ packages/core/src/systems/en-US/index.ts | 2 + .../src/systems/{en-US.ts => en-US/parse.ts} | 443 +----------------- packages/core/src/systems/en-US/stringify.ts | 269 +++++++++++ 4 files changed, 469 insertions(+), 417 deletions(-) create mode 100644 packages/core/src/systems/en-US/common.ts create mode 100644 packages/core/src/systems/en-US/index.ts rename packages/core/src/systems/{en-US.ts => en-US/parse.ts} (53%) create mode 100644 packages/core/src/systems/en-US/stringify.ts diff --git a/packages/core/src/systems/en-US/common.ts b/packages/core/src/systems/en-US/common.ts new file mode 100644 index 0000000..bff1878 --- /dev/null +++ b/packages/core/src/systems/en-US/common.ts @@ -0,0 +1,172 @@ +import { Group } from '../../common'; + +export const DECIMAL_POINT = '.' as const; + +export const GROUPING_SYMBOL = ',' as const; + +export const NEGATIVE = 'negative' as const; + +export const NEGATIVE_SYMBOL = '-' as const; + +// replace with hyphen with option +export const TENS_ONES_SEPARATOR = ' ' as const; + +export const POSITIVE_SYMBOL = '+' as const; + +export const SHORT_MILLIA_DELIMITER = '^' as const; + +export const EXPONENT_DELIMITER = 'e' as const; + +export const EMPTY_GROUP_DIGITS = '000' as const; + +export const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, 0]; + +/** + * Ones number names. + */ +export const ONES = [ + 'zero', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', +] as const; + +export type OnesName = typeof ONES[number]; + +/** + * Ten plus ones number names. + */ +export const TEN_PLUS_ONES = [ + 'ten', + 'eleven', + 'twelve', + 'thirteen', + 'fourteen', + 'fifteen', + 'sixteen', + 'seventeen', + 'eighteen', + 'nineteen', +] as const; + +export type TenPlusOnesName = typeof TEN_PLUS_ONES[number]; + +/** + * Tens number names. + */ +export const TENS = [ + 'zero', + TEN_PLUS_ONES[0], + 'twenty', + 'thirty', + 'forty', + 'fifty', + 'sixty', + 'seventy', + 'eighty', + 'ninety', +] as const; + +export type TensName = typeof TENS[number]; + +/** + * Hundreds name. + */ +export const HUNDRED = 'hundred' as const; + +/** + * Thousands name. + */ +export const THOUSAND = 'thousand' as const; + +// export const ILLION_ORDINAL_SUFFIX = 'illionth' as const; + +// export const THOUSAND_ORDINAL = 'thousandth' as const; + +/** + * Special millions name. + */ +export const MILLIONS_SPECIAL_PREFIXES = [ + '', + 'm', + 'b', + 'tr', + 'quadr', + 'quint', + 'sext', + 'sept', + 'oct', + 'non', +] as const; + +export type MillionsSpecialPrefix = Exclude; + +/** + * Millions name. + */ +export const MILLIONS_PREFIXES = [ + '', + 'un', + 'duo', + 'tre', + 'quattuor', + 'quin', + 'sex', + 'septen', + 'octo', + 'novem', +] as const; + +export type MillionsPrefix = Exclude; + +/** + * Decillions name. + */ +export const DECILLIONS_PREFIXES = [ + '', + 'dec', + 'vigin', + 'trigin', + 'quadragin', + 'quinquagin', + 'sexagin', + 'septuagin', + 'octogin', + 'nonagin', +] as const; + +export type DecillionsPrefix = Exclude; + +/** + * Centillions name. + */ +export const CENTILLIONS_PREFIXES = [ + '', + 'cen', + 'duocen', + 'trecen', + 'quadringen', + 'quingen', + 'sescen', + 'septingen', + 'octingen', + 'nongen', +] as const; + +export type CentillionsPrefix = Exclude; + +/** + * Prefix for millia- number names. + */ +export const MILLIA_PREFIX = 'millia' as const; + +/** + * Suffix for -illion number names. + */ +export const ILLION_SUFFIX = 'illion' as const; diff --git a/packages/core/src/systems/en-US/index.ts b/packages/core/src/systems/en-US/index.ts new file mode 100644 index 0000000..7344573 --- /dev/null +++ b/packages/core/src/systems/en-US/index.ts @@ -0,0 +1,2 @@ +export * from './parse'; +export * from './stringify'; diff --git a/packages/core/src/systems/en-US.ts b/packages/core/src/systems/en-US/parse.ts similarity index 53% rename from packages/core/src/systems/en-US.ts rename to packages/core/src/systems/en-US/parse.ts index 8bb8c1f..884c679 100644 --- a/packages/core/src/systems/en-US.ts +++ b/packages/core/src/systems/en-US/parse.ts @@ -1,424 +1,35 @@ -// noinspection SpellCheckingInspection - import { Group, GROUP_DIGITS_INDEX, GROUP_PLACE_INDEX, 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; - -// replace with hyphen with option -const TENS_ONES_SEPARATOR = ' ' 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; - -/** - * Millions name. - */ -const MILLIONS_PREFIXES = [ - '', - 'un', - 'duo', - 'tre', - 'quattuor', - 'quin', - 'sex', - 'septen', - 'octo', - 'novem', -] as const; - -type MillionsPrefix = Exclude; - -/** - * Decillions name. - */ -const DECILLIONS_PREFIXES = [ - '', - 'dec', - 'vigin', - 'trigin', - 'quadragin', - 'quinquagin', - 'sexagin', - 'septuagin', - 'octogin', - 'nonagin', -] as const; - -type DecillionsPrefix = Exclude; - -/** - * Centillions name. - */ -const CENTILLIONS_PREFIXES = [ - '', - 'cen', - 'duocen', - 'trecen', - 'quadringen', - 'quingen', - 'sescen', - 'septingen', - 'octingen', - 'nongen', -] as const; - -type CentillionsPrefix = Exclude; - -/** - * 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 string 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}${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. - * @returns string 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 string 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 string 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 string 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( - (acc, c, i, cc) => { - const firstGroup = acc.at(0); - const currentPlace = 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}` - : 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 makeGroups = (groups: Group[], options?: Record): string[] => { - const filteredGroups = groups.filter(([digits, place]) => ( - place === 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); - const groupName = getGroupName(place, options?.shortenMillia as boolean ?? 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 = 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() -); +} from '../../common'; +import { + CENTILLIONS_PREFIXES, + DECILLIONS_PREFIXES, + DECIMAL_POINT, + EMPTY_GROUP_DIGITS, + EMPTY_PLACE, + EXPONENT_DELIMITER, + HUNDRED, + ILLION_SUFFIX, + MILLIA_PREFIX, + MILLIONS_PREFIXES, + MILLIONS_SPECIAL_PREFIXES, + NEGATIVE_SYMBOL, + ONES, + OnesName, + POSITIVE_SYMBOL, + SHORT_MILLIA_DELIMITER, + TEN_PLUS_ONES, + TenPlusOnesName, + TENS, + TENS_ONES_SEPARATOR, + TensName, + THOUSAND, +} from './common'; -/** - * Makes a negative string. - * @param s - The string to make negative. - */ -export const makeNegative = (s: string) => ( - `${NEGATIVE} ${s}` -); +const FINAL_TOKEN = '' as const; export const tokenize = (stringValue: string) => ( stringValue @@ -427,8 +38,6 @@ export const tokenize = (stringValue: string) => ( .filter((maybeToken) => maybeToken.length > 0) ); -const FINAL_TOKEN = '' as const; - interface DoParseState { groupNameCurrent: string; millias: number[]; diff --git a/packages/core/src/systems/en-US/stringify.ts b/packages/core/src/systems/en-US/stringify.ts new file mode 100644 index 0000000..e79d906 --- /dev/null +++ b/packages/core/src/systems/en-US/stringify.ts @@ -0,0 +1,269 @@ +import { Group, GROUP_DIGITS_INDEX, GROUP_PLACE_INDEX } 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. + * @returns string 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}${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. + * @returns string 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 string 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 string 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 string 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( + (acc, c, i, cc) => { + const firstGroup = acc.at(0); + const currentPlace = 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}` + : 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 makeGroups = (groups: Group[], options?: Record): string[] => { + const filteredGroups = groups.filter(([digits, place]) => ( + place === 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); + const groupName = getGroupName(place, options?.shortenMillia as boolean ?? 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 = 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}` +);