@@ -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 { | |||
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<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) => ( | |||
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[]; |
@@ -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}` | |||
); |