Put functions with different purposes into their own files.master
@@ -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<typeof MILLIONS_SPECIAL_PREFIXES[number], ''>; | |||||
/** | |||||
* Millions name. | |||||
*/ | |||||
export const MILLIONS_PREFIXES = [ | |||||
'', | |||||
'un', | |||||
'duo', | |||||
'tre', | |||||
'quattuor', | |||||
'quin', | |||||
'sex', | |||||
'septen', | |||||
'octo', | |||||
'novem', | |||||
] as const; | |||||
export type MillionsPrefix = Exclude<typeof MILLIONS_PREFIXES[number], ''>; | |||||
/** | |||||
* Decillions name. | |||||
*/ | |||||
export const DECILLIONS_PREFIXES = [ | |||||
'', | |||||
'dec', | |||||
'vigin', | |||||
'trigin', | |||||
'quadragin', | |||||
'quinquagin', | |||||
'sexagin', | |||||
'septuagin', | |||||
'octogin', | |||||
'nonagin', | |||||
] as const; | |||||
export type DecillionsPrefix = Exclude<typeof DECILLIONS_PREFIXES[number], ''>; | |||||
/** | |||||
* Centillions name. | |||||
*/ | |||||
export const CENTILLIONS_PREFIXES = [ | |||||
'', | |||||
'cen', | |||||
'duocen', | |||||
'trecen', | |||||
'quadringen', | |||||
'quingen', | |||||
'sescen', | |||||
'septingen', | |||||
'octingen', | |||||
'nongen', | |||||
] as const; | |||||
export type CentillionsPrefix = Exclude<typeof CENTILLIONS_PREFIXES[number], ''>; | |||||
/** | |||||
* Prefix for millia- number names. | |||||
*/ | |||||
export const MILLIA_PREFIX = 'millia' as const; | |||||
/** | |||||
* Suffix for -illion number names. | |||||
*/ | |||||
export const ILLION_SUFFIX = 'illion' as const; |
@@ -0,0 +1,2 @@ | |||||
export * from './parse'; | |||||
export * from './stringify'; |
@@ -1,424 +1,35 @@ | |||||
// noinspection SpellCheckingInspection | |||||
import { | import { | ||||
Group, | Group, | ||||
GROUP_DIGITS_INDEX, | GROUP_DIGITS_INDEX, | ||||
GROUP_PLACE_INDEX, | GROUP_PLACE_INDEX, | ||||
InvalidTokenError, | 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<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 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<TensName, 'zero' | 'ten'>}${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. | |||||
* @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<Group[]>( | |||||
(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, unknown>): 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<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[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) => ( | export const tokenize = (stringValue: string) => ( | ||||
stringValue | stringValue | ||||
@@ -427,8 +38,6 @@ export const tokenize = (stringValue: string) => ( | |||||
.filter((maybeToken) => maybeToken.length > 0) | .filter((maybeToken) => maybeToken.length > 0) | ||||
); | ); | ||||
const FINAL_TOKEN = '' as const; | |||||
interface DoParseState { | interface DoParseState { | ||||
groupNameCurrent: string; | groupNameCurrent: string; | ||||
millias: number[]; | millias: number[]; |
@@ -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<TensName, 'zero' | 'ten'>}${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. | |||||
* @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<Group[]>( | |||||
(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, unknown>): 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<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[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}` | |||||
); |