|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- import {
- bigIntMax, bigIntMin,
- Group,
- GROUP_DIGITS_INDEX,
- GROUP_PLACE_INDEX, GroupDigits,
- InvalidTokenError,
- } 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,
- NEGATIVE_SYMBOL,
- ONES,
- OnesName,
- POSITIVE_SYMBOL,
- SHORT_MILLIA_DELIMITER,
- SHORT_MILLIA_ILLION_DELIMITER,
- T_AFFIX,
- TEN_PLUS_ONES,
- TenPlusOnesName,
- TENS,
- TENS_ONES_SEPARATOR,
- TensName,
- THOUSAND,
- } from '../../en/common';
-
- const FINAL_TOKEN = '' as const;
-
- /**
- * Tokenizes a string.
- * @param value - The string to tokenize.
- * @see {NumberNameSystem.mergeTokens}
- * @returns string[] The tokens.
- */
- export const tokenize = (value: string) => (
- value
- .toLowerCase()
- .trim()
- .replace(/\n+/gs, ' ')
- .replace(/\s+/g, ' ')
- .replace(
- new RegExp(`${THOUSAND}\\s+(.+?${ILLION_SUFFIX})`, 'g'),
- (_substring, illion: string) => (
- `${THOUSAND}${illion}`
- ),
- )
- .replace(
- new RegExp(`${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(\\d+)${SHORT_MILLIA_ILLION_DELIMITER}`, 'g'),
- (_substring, milliaCount: string) => `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${milliaCount}`,
- )
- .replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ')
- .split(' ')
- .filter((maybeToken) => maybeToken.length > 0)
- );
-
- interface DoParseState {
- groupNameCurrent: string;
- millias: number[];
- milliaIndex: number;
- done: boolean;
- }
-
- /**
- * Deconstructs a group name token (e.g. "million", "duodecillion", etc.) to its affixes and
- * parses them.
- * @param result - The current state of the parser.
- * @returns DoParseState The next state of the parser.
- */
- const doParseGroupName = (result: DoParseState): DoParseState => {
- if (
- result.groupNameCurrent.length < 1
-
- // 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.
- || result.groupNameCurrent === T_AFFIX
- ) {
- return {
- ...result,
-
- // Fill the gaps of millias with zeros.
- millias: new Array(result.millias.length)
- .fill(0)
- .map((z, i) => (
- result.millias[i] ?? z
- )),
- 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)) {
- let newMillia: number;
- let prefix: string;
- const isShortMillia = result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`);
- if (isShortMillia) {
- const matchedMilliaArray = result.groupNameCurrent
- .match(new RegExp(`^${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(\\d+)`));
- if (!matchedMilliaArray) {
- throw new InvalidTokenError(result.groupNameCurrent);
- }
- const [wholeMilliaPrefix, matchedMillia] = matchedMilliaArray;
- newMillia = Number(matchedMillia);
- prefix = wholeMilliaPrefix;
- } else {
- newMillia = result.milliaIndex + 1;
- prefix = MILLIA_PREFIX;
- }
- 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(prefix.length),
- done: false,
- };
- }
-
- throw new InvalidTokenError(result.groupNameCurrent);
- };
-
- /**
- * Gets the place of a group name (e.g. "million", "duodecillion", etc.).
- * @param groupName - The group name.
- * @returns bigint The place of the group name.
- */
- const getGroupPlaceFromGroupName = (groupName: string) => {
- if (groupName === THOUSAND) {
- return BigInt(1);
- }
-
- const groupNameBase = groupName.replace(ILLION_SUFFIX, '').replace(THOUSAND, '');
- const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);
-
- if (specialMillions > -1) {
- return BigInt(specialMillions * 2) + (groupName.startsWith(THOUSAND) ? BigInt(1) : BigInt(0));
- }
-
- let result: DoParseState = {
- groupNameCurrent: groupNameBase,
- millias: [0],
- milliaIndex: 0,
- done: false,
- };
-
- do {
- result = doParseGroupName(result);
- } while (!result.done);
-
- const bigGroupPlace = BigInt(
- result.millias
- .map((s) => s.toString().padStart(3, '0'))
- .reverse()
- .join(''),
- );
-
- return bigGroupPlace * BigInt(2) + (groupName.startsWith(THOUSAND) ? BigInt(1) : BigInt(0));
- };
-
- /**
- * Mode of the group parser.
- */
- enum ParseGroupsMode {
- /**
- * Initial mode.
- */
- INITIAL = 'initial',
- /**
- * Has parsed a ones name.
- */
- ONES_MODE = 'ones',
- /**
- * Has parsed a tens name.
- */
- TENS_MODE = 'tens',
- /**
- * Has parsed a ten-plus-ones name.
- */
- TEN_PLUS_ONES_MODE = 'tenPlusOnes',
- /**
- * Has parsed a "hundred" token.
- */
- HUNDRED_MODE = 'hundred',
- /**
- * Has parsed a "thousand" or any "-illion" token.
- */
- THOUSAND_MODE = 'thousand',
- /**
- * Done parsing.
- */
- DONE = 'done',
- }
-
- /**
- * State of the group parser.
- */
- interface ParserState {
- lastToken?: string;
- groups: Group[];
- mode: ParseGroupsMode;
- negative: boolean;
- }
-
- const parseThousand = (acc: ParserState, token: string): ParserState => {
- const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
-
- if (acc.mode === ParseGroupsMode.ONES_MODE) {
- const ones = ONES.findIndex((o) => o === acc.lastToken);
- if (ones > -1) {
- lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
- }
- } else if (acc.mode === ParseGroupsMode.TENS_MODE) {
- const tens = TENS.findIndex((t) => t === acc.lastToken);
- if (tens > -1) {
- lastGroup[GROUP_DIGITS_INDEX] = (
- `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
- ) as GroupDigits;
- }
- }
-
- // Put the digits in the right place.
- lastGroup[GROUP_PLACE_INDEX] = getGroupPlaceFromGroupName(token);
-
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- lastToken: token,
- mode: ParseGroupsMode.THOUSAND_MODE,
- };
- };
-
- const parseHundred = (acc: ParserState): ParserState => {
- const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
- const hundreds = ONES.findIndex((o) => o === acc.lastToken);
- lastGroup[GROUP_DIGITS_INDEX] = `${hundreds}${lastGroup[GROUP_DIGITS_INDEX].slice(1)}` as GroupDigits;
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- mode: ParseGroupsMode.HUNDRED_MODE,
- };
- };
-
- const parseFinal = (acc: ParserState): ParserState => {
- const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
-
- if (acc.mode === ParseGroupsMode.ONES_MODE) {
- const ones = ONES.findIndex((o) => o === acc.lastToken);
- if (ones > -1) {
- lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
- }
- // We assume last token without parsed place will always be the smallest
- lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- mode: ParseGroupsMode.DONE,
- };
- }
-
- if (acc.mode === ParseGroupsMode.TENS_MODE) {
- const tens = TENS.findIndex((o) => o === acc.lastToken);
- if (tens > -1) {
- lastGroup[GROUP_DIGITS_INDEX] = (
- `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
- ) as GroupDigits;
- }
- lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
- return {
- ...acc,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- mode: ParseGroupsMode.DONE,
- };
- }
-
- return acc;
- };
-
- const parseOnes = (acc: ParserState, token: string): ParserState => {
- if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
- // Create next empty place
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.ONES_MODE,
- groups: [...acc.groups, [...EMPTY_PLACE]],
- };
- }
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.ONES_MODE,
- };
- };
-
- const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => {
- const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
- const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
- if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
- groups: [...acc.groups, [`01${tenPlusOnes}` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
- };
- }
-
- lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}` as GroupDigits;
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- };
- };
-
- const parseTens = (acc: ParserState, token: string): ParserState => {
- const tens = TENS.findIndex((t) => t === token);
- const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
- if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TENS_MODE,
- groups: [...acc.groups, [`0${tens}0` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
- };
- }
-
- lastGroup[GROUP_DIGITS_INDEX] = (
- `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
- ) as GroupDigits;
-
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TENS_MODE,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- };
- };
-
- /**
- * Parses groups from a string.
- * @param tokens - The string to parse groups from.
- * @see {NumberNameSystem.stringifyGroups}
- * @returns Group[] The parsed groups.
- */
- export const parseGroups = (tokens: string[]) => {
- // We add a final token which is an empty string to parse whatever the last non-empty token is.
- const tokensToParse = [...tokens, FINAL_TOKEN];
- const { groups, negative } = tokensToParse.reduce<ParserState>(
- (acc, token) => {
- if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
- return parseThousand(acc, token);
- }
-
- if (token === HUNDRED && acc.mode === ParseGroupsMode.ONES_MODE) {
- return parseHundred(acc);
- }
-
- if (token === FINAL_TOKEN) {
- return parseFinal(acc);
- }
-
- if (ONES.includes(token as OnesName)) {
- return parseOnes(acc, token);
- }
-
- if (TEN_PLUS_ONES.includes(token as TenPlusOnesName)) {
- return parseTenPlusOnes(acc, token);
- }
-
- if (TENS.includes(token as TensName)) {
- return parseTens(acc, token);
- }
-
- if (token === NEGATIVE) {
- return {
- ...acc,
- negative: !acc.negative,
- };
- }
-
- return {
- ...acc,
- lastToken: token,
- };
- },
- {
- lastToken: undefined,
- groups: [],
- mode: ParseGroupsMode.INITIAL,
- negative: false,
- },
- );
-
- return { groups, negative };
- };
-
- /**
- * Combines groups into a string.
- * @param groups - The groups to combine.
- * @param negative - Whether the number is negative.
- * @see {NumberNameSystem.splitIntoGroups}
- * @returns string The combined groups in exponential form.
- */
- export const combineGroups = (groups: Group[], negative: boolean) => {
- if (groups.length < 1) {
- return '';
- }
- const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
- const maxPlace = bigIntMax(...places) as bigint;
- const minPlace = bigIntMin(...places) as bigint;
- const firstGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === maxPlace) ?? [...EMPTY_PLACE];
- const firstGroupPlace = firstGroup[GROUP_PLACE_INDEX];
- const groupsSorted = [];
- for (let i = maxPlace; i >= minPlace; i = BigInt(i) - BigInt(1)) {
- const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [EMPTY_GROUP_DIGITS, i];
- groupsSorted.push(thisGroup);
- }
-
- 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(
- (BigInt(firstGroupPlace) * BigInt(3)) + (BigInt(2) - BigInt(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 `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
- }
- return `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
- };
|