Browse Source

Refactor groups

Make group management more readable.
master
TheoryOfNekomata 1 year ago
parent
commit
6a78aecef8
3 changed files with 183 additions and 87 deletions
  1. +10
    -0
      packages/core/src/common.ts
  2. +163
    -87
      packages/core/src/systems/en-US.ts
  3. +10
    -0
      packages/core/test/systems/en-US.test.ts

+ 10
- 0
packages/core/src/common.ts View File

@@ -35,6 +35,16 @@ type GroupPlace = number;
*/
export type Group = [GroupDigits, GroupPlace];

/**
* Index of the group digits in a {@link Group|group}.
*/
export const GROUP_DIGITS_INDEX = 0 as const;

/**
* Index of the group place in a {@link Group|group}.
*/
export const GROUP_PLACE_INDEX = 1 as const;

/**
* System for stringifying and parsing numbers.
*/


+ 163
- 87
packages/core/src/systems/en-US.ts View File

@@ -1,6 +1,11 @@
// noinspection SpellCheckingInspection

import { Group, InvalidTokenError } from '../common';
import {
Group,
GROUP_DIGITS_INDEX,
GROUP_PLACE_INDEX,
InvalidTokenError,
} from '../common';
import { numberToExponential } from '../exponent';

const DECIMAL_POINT = '.' as const;
@@ -287,16 +292,20 @@ const getGroupName = (place: number, shortenMillia: boolean) => {
(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') {
return [[c, currentPlace]];
newGroup[GROUP_DIGITS_INDEX] = c;
return [newGroup];
}

if (firstGroup[0].length > 2) {
return [[c, currentPlace], ...acc];
newGroup[GROUP_DIGITS_INDEX] = c;
return [newGroup, ...acc];
}

newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
return [
[c + firstGroup[0], currentPlace],
newGroup,
...acc.slice(1),
];
},
@@ -378,7 +387,7 @@ export const group = (value: string): Group[] => {
const currentPlace = Math.floor((exponent - i) / 3);
const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
const currentPlaceInGroup = 2 - ((exponent - i) % 3);
if (lastGroup[1] === currentPlace) {
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
const lastGroupDigits = lastGroup[0].split('');
lastGroupDigits[currentPlaceInGroup] = c;
return [...acc.slice(0, -1) ?? [], [
@@ -584,99 +593,166 @@ interface ParserState {
mode: ParseGroupsMode;
}

export const parseGroups = (tokens: string[]) => {
const { groups } = [...tokens, FINAL_TOKEN].reduce<ParserState>(
(acc, token) => {
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
const parseThousand = (acc: ParserState, token: string): ParserState => {
const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];

if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
if (acc.mode === ParseGroupsMode.ONES_MODE) {
const ones = ONES.findIndex((o) => o === acc.lastToken);
if (ones > -1) {
lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
}
}
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;
};

lastGroup[1] = getGroupPlaceFromGroupName(token);
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]],
};
}

return {
...acc,
groups: [...acc.groups.slice(0, -1), lastGroup],
lastToken: token,
mode: ParseGroupsMode.THOUSAND_MODE,
};
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) {
if (acc.mode === ParseGroupsMode.ONES_MODE) {
const hundreds = ONES.findIndex((o) => o === acc.lastToken);
lastGroup[0] = `${hundreds}${lastGroup[0].slice(1)}`;
return {
...acc,
groups: [...acc.groups.slice(0, -1), lastGroup],
mode: ParseGroupsMode.HUNDRED_MODE,
};
}
if (token === HUNDRED && acc.mode === ParseGroupsMode.ONES_MODE) {
return parseHundred(acc);
}

if (token === FINAL_TOKEN) {
if (acc.mode === ParseGroupsMode.ONES_MODE) {
const ones = ONES.findIndex((o) => o === acc.lastToken);
lastGroup[0] = `${lastGroup[0].slice(0, 2)}${ones}`;
lastGroup[1] = 0;
return {
...acc,
groups: [...acc.groups.slice(0, -1), lastGroup],
mode: ParseGroupsMode.DONE,
};
}
return parseFinal(acc);
}

if (ONES.includes(token as OnesName)) {
if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
return {
...acc,
lastToken: token,
mode: ParseGroupsMode.ONES_MODE,
groups: [...acc.groups, [...EMPTY_PLACE]],
};
}
return {
...acc,
lastToken: token,
mode: ParseGroupsMode.ONES_MODE,
};
return parseOnes(acc, token);
}

const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
if (tenPlusOnes > -1) {
if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
return {
...acc,
lastToken: token,
mode: ParseGroupsMode.ONES_MODE,
groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[1] - 1]],
};
}

lastGroup[0] = `${lastGroup[0].slice(0, 1)}1${tenPlusOnes}`;
return {
...acc,
lastToken: token,
mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
groups: [...acc.groups.slice(0, -1), lastGroup],
};
if (TEN_PLUS_ONES.includes(token as TenPlusOnesName)) {
return parseTenPlusOnes(acc, token);
}

const tens = TENS.findIndex((t) => t === token);
if (tens > -1) {
lastGroup[0] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[0].slice(2)}`;
return {
...acc,
lastToken: token,
mode: ParseGroupsMode.TENS_MODE,
groups: [...acc.groups.slice(0, -1), lastGroup],
};
if (TENS.includes(token as TensName)) {
return parseTens(acc, token);
}

return {
@@ -695,14 +771,14 @@ export const parseGroups = (tokens: string[]) => {
};

export const combineGroups = (groups: Group[]) => {
const places = groups.map((g) => g[1]);
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[1] === maxPlace) ?? [...EMPTY_PLACE];
const firstGroupPlace = firstGroup[1];
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[1] === i) ?? [...EMPTY_PLACE];
const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [...EMPTY_PLACE];
groupsSorted.push(thisGroup);
}



+ 10
- 0
packages/core/test/systems/en-US.test.ts View File

@@ -263,5 +263,15 @@ describe('numerica', () => {
const exp2 = numberToExponential(value2);
expect(stringify(value2)).toBe('one trillion five million');
expect(parse(stringify(value2))).toBe(exp2);

const value3 = '12012';
const exp3 = numberToExponential(value3);
expect(stringify(value3)).toBe('twelve thousand twelve');
expect(parse(stringify(value3))).toBe(exp3);

const value4 = '12020';
const exp4 = numberToExponential(value4);
expect(stringify(value4)).toBe('twelve thousand twenty');
expect(parse(stringify(value4))).toBe(exp4);
});
});

Loading…
Cancel
Save