|
- import {
- Group,
- GROUP_DIGITS_INDEX,
- GROUP_PLACE_INDEX,
- 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_SYMBOL,
- ONES,
- OnesName,
- POSITIVE_SYMBOL,
- SHORT_MILLIA_DELIMITER,
- TEN_PLUS_ONES,
- TenPlusOnesName,
- TENS,
- TENS_ONES_SEPARATOR,
- TensName,
- THOUSAND,
- } from './common';
-
- const FINAL_TOKEN = '' as const;
-
- export const tokenize = (stringValue: string) => (
- stringValue
- .replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ')
- .split(' ')
- .filter((maybeToken) => maybeToken.length > 0)
- );
-
- interface DoParseState {
- groupNameCurrent: string;
- millias: number[];
- milliaIndex: number;
- done: boolean;
- }
-
- const doParseGroupName = (result: DoParseState): DoParseState => {
- if (result.groupNameCurrent.length < 1) {
- return {
- ...result,
- done: true,
- };
- }
-
- if (result.groupNameCurrent === 't') {
- // 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.
- return {
- ...result,
- 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}${SHORT_MILLIA_DELIMITER}`)) {
- // short millia
- const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/);
- if (!matchedMilliaArray) {
- throw new InvalidTokenError(result.groupNameCurrent);
- }
- const matchedMillia = matchedMilliaArray[0];
- const newMillia = Number(matchedMillia);
- 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(MILLIA_PREFIX.length),
- done: false,
- };
- }
-
- if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) {
- const newMillia = result.milliaIndex + 1;
- 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(MILLIA_PREFIX.length),
- done: false,
- };
- }
-
- throw new InvalidTokenError(result.groupNameCurrent);
- };
-
- const getGroupPlaceFromGroupName = (groupName: string) => {
- if (groupName === THOUSAND) {
- return 1;
- }
-
- const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
- const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);
-
- if (specialMillions > -1) {
- return specialMillions + 1;
- }
-
- let result: DoParseState = {
- groupNameCurrent: groupNameBase,
- millias: [0],
- milliaIndex: 0,
- done: false,
- };
-
- do {
- result = doParseGroupName(result);
- } while (!result.done);
-
- const bigGroupPlace = Number(
- result.millias
- .map((s) => s.toString().padStart(3, '0'))
- .reverse()
- .join(''),
- );
-
- return bigGroupPlace + 1;
- };
-
- enum ParseGroupsMode {
- INITIAL = 'unknown',
- ONES_MODE = 'ones',
- TENS_MODE = 'tens',
- TEN_PLUS_ONES_MODE = 'tenPlusOnes',
- HUNDRED_MODE = 'hundred',
- THOUSAND_MODE = 'thousand',
- DONE = 'done',
- }
-
- interface ParserState {
- lastToken?: string;
- groups: Group[];
- mode: ParseGroupsMode;
- }
-
- 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}`;
- }
- } 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)}`
- );
- }
- }
-
- // 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)}`;
- 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}`;
- }
- // We assume last token without parsed place will always be the smallest
- lastGroup[GROUP_PLACE_INDEX] = 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)}`;
- }
- lastGroup[GROUP_PLACE_INDEX] = 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}`, lastGroup[GROUP_PLACE_INDEX] - 1]],
- };
- }
-
- lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}`;
- 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`, lastGroup[GROUP_PLACE_INDEX] - 1]],
- };
- }
-
- lastGroup[GROUP_DIGITS_INDEX] = (
- `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
- );
- return {
- ...acc,
- lastToken: token,
- mode: ParseGroupsMode.TENS_MODE,
- groups: [...acc.groups.slice(0, -1), lastGroup],
- };
- };
-
- 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 } = 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);
- }
-
- return {
- ...acc,
- lastToken: token,
- };
- },
- {
- lastToken: undefined,
- groups: [],
- mode: ParseGroupsMode.INITIAL,
- },
- );
-
- return groups;
- };
-
- export const combineGroups = (groups: Group[]) => {
- const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
- const maxPlace = Math.max(...places);
- const minPlace = Math.min(...places);
- 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 -= 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((firstGroupPlace * 3) + (2 - 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 `${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
- }
- return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
- };
|