Browse Source

Add edge cases

Add tests related to error handling.
master
TheoryOfNekomata 1 year ago
parent
commit
a66479fe60
8 changed files with 159 additions and 31 deletions
  1. +17
    -2
      packages/core/src/common.ts
  2. +2
    -2
      packages/core/src/converter.ts
  3. +18
    -8
      packages/core/src/systems/en-UK/long-count/parse.ts
  4. +29
    -18
      packages/core/src/systems/en-US/short-count/parse.ts
  5. +1
    -1
      packages/core/src/systems/en-US/short-count/stringify.ts
  6. +29
    -0
      packages/core/test/systems/en-UK/long-count.test.ts
  7. +32
    -0
      packages/core/test/systems/en-UK/short-count.test.ts
  8. +31
    -0
      packages/core/test/systems/en-US/short-count.test.ts

+ 17
- 2
packages/core/src/common.ts View File

@@ -45,6 +45,20 @@ export const GROUP_DIGITS_INDEX = 0 as const;
*/
export const GROUP_PLACE_INDEX = 1 as const;

/**
* Result of parsing a number string.
*/
export interface ParseResult {
/**
* The parsed groups.
*/
groups: Group[];
/**
* Whether the number is negative.
*/
negative: boolean;
}

/**
* System for stringifying and parsing numbers.
*/
@@ -90,14 +104,15 @@ export interface NumberNameSystem {
* @see {NumberNameSystem.stringifyGroups}
* @returns Group[] The parsed groups.
*/
parseGroups: (tokens: string[]) => Group[];
parseGroups: (tokens: string[]) => ParseResult;
/**
* 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.
*/
combineGroups: (groups: Group[]) => string;
combineGroups: (groups: Group[], negative: boolean) => string;
}

/**


+ 2
- 2
packages/core/src/converter.ts View File

@@ -113,8 +113,8 @@ export const parse = (value: string, options = {} as ParseOptions) => {
const { system = enUS.shortCount, type = 'string' } = options;

const tokens = system.tokenize(value);
const groups = system.parseGroups(tokens);
const stringValue = system.combineGroups(groups);
const { groups, negative } = system.parseGroups(tokens);
const stringValue = system.combineGroups(groups, negative);

switch (type) {
case 'number': {


+ 18
- 8
packages/core/src/systems/en-UK/long-count/parse.ts View File

@@ -16,7 +16,7 @@ import {
ILLION_SUFFIX,
MILLIA_PREFIX,
MILLIONS_PREFIXES,
MILLIONS_SPECIAL_PREFIXES,
MILLIONS_SPECIAL_PREFIXES, NEGATIVE,
NEGATIVE_SYMBOL,
ONES,
OnesName,
@@ -260,6 +260,7 @@ interface ParserState {
lastToken?: string;
groups: Group[];
mode: ParseGroupsMode;
negative: boolean;
}

const parseThousand = (acc: ParserState, token: string): ParserState => {
@@ -404,7 +405,7 @@ const parseTens = (acc: ParserState, token: string): ParserState => {
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>(
const { groups, negative } = tokensToParse.reduce<ParserState>(
(acc, token) => {
if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
return parseThousand(acc, token);
@@ -430,6 +431,13 @@ export const parseGroups = (tokens: string[]) => {
return parseTens(acc, token);
}

if (token === NEGATIVE) {
return {
...acc,
negative: !acc.negative,
};
}

return {
...acc,
lastToken: token,
@@ -439,23 +447,25 @@ export const parseGroups = (tokens: string[]) => {
lastToken: undefined,
groups: [],
mode: ParseGroupsMode.INITIAL,
negative: false,
},
);

return groups;
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[]) => {
const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
if (places.length < 1) {
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];
@@ -486,7 +496,7 @@ export const combineGroups = (groups: Group[]) => {
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 `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
}
return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
return `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
};

+ 29
- 18
packages/core/src/systems/en-US/short-count/parse.ts View File

@@ -16,7 +16,7 @@ import {
ILLION_SUFFIX,
MILLIA_PREFIX,
MILLIONS_PREFIXES,
MILLIONS_SPECIAL_PREFIXES,
MILLIONS_SPECIAL_PREFIXES, NEGATIVE,
NEGATIVE_SYMBOL,
ONES,
OnesName,
@@ -47,7 +47,7 @@ export const tokenize = (value: string) => (
.replace(/\n+/gs, ' ')
.replace(/\s+/g, ' ')
.replace(
new RegExp(`${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(\\d+)${SHORT_MILLIA_ILLION_DELIMITER}`, 'g'),
new RegExp(`${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(.+?)${SHORT_MILLIA_ILLION_DELIMITER}`, 'g'),
(_substring, milliaCount: string) => `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${milliaCount}`,
)
.replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ')
@@ -66,9 +66,10 @@ interface DoParseState {
* Deconstructs a group name token (e.g. "million", "duodecillion", etc.) to its affixes and
* parses them.
* @param result - The current state of the parser.
* @param originalGroupName - The original group name token.
* @returns DoParseState The next state of the parser.
*/
const doParseGroupName = (result: DoParseState): DoParseState => {
const doParseGroupName = (result: DoParseState, originalGroupName: string): DoParseState => {
if (
result.groupNameCurrent.length < 1

@@ -151,10 +152,13 @@ const doParseGroupName = (result: DoParseState): DoParseState => {
const matchedMilliaArray = result.groupNameCurrent
.match(new RegExp(`^${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(\\d+)`));
if (!matchedMilliaArray) {
throw new InvalidTokenError(result.groupNameCurrent);
throw new InvalidTokenError(originalGroupName);
}
const [wholeMilliaPrefix, matchedMillia] = matchedMilliaArray;
newMillia = Number(matchedMillia);
if (newMillia < 1) {
throw new InvalidTokenError(originalGroupName);
}
prefix = wholeMilliaPrefix;
} else {
newMillia = result.milliaIndex + 1;
@@ -172,7 +176,7 @@ const doParseGroupName = (result: DoParseState): DoParseState => {
};
}

throw new InvalidTokenError(result.groupNameCurrent);
throw new InvalidTokenError(originalGroupName);
};

/**
@@ -200,7 +204,7 @@ const getGroupPlaceFromGroupName = (groupName: string) => {
};

do {
result = doParseGroupName(result);
result = doParseGroupName(result, groupName);
} while (!result.done);

const bigGroupPlace = BigInt(
@@ -254,6 +258,7 @@ interface ParserState {
lastToken?: string;
groups: Group[];
mode: ParseGroupsMode;
negative: boolean;
}

const parseThousand = (acc: ParserState, token: string): ParserState => {
@@ -398,7 +403,7 @@ const parseTens = (acc: ParserState, token: string): ParserState => {
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>(
const { groups, negative } = tokensToParse.reduce<ParserState>(
(acc, token) => {
if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
return parseThousand(acc, token);
@@ -424,35 +429,41 @@ export const parseGroups = (tokens: string[]) => {
return parseTens(acc, token);
}

return {
...acc,
lastToken: token,
};
if (token === NEGATIVE) {
return {
...acc,
negative: !acc.negative,
};
}

throw new InvalidTokenError(token);
},
{
lastToken: undefined,
groups: [],
mode: ParseGroupsMode.INITIAL,
negative: false,
},
);

return groups;
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[]) => {
const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
if (places.length < 1) {
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 firstGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === maxPlace) as Group;
const firstGroupPlace = firstGroup[GROUP_PLACE_INDEX];
const groupsSorted = [];
for (let i = maxPlace; i >= minPlace; i = BigInt(i) - BigInt(1)) {
@@ -480,7 +491,7 @@ export const combineGroups = (groups: Group[]) => {
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 `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
}
return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
return `${negative ? NEGATIVE_SYMBOL : ''}${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
};

+ 1
- 1
packages/core/src/systems/en-US/short-count/stringify.ts View File

@@ -306,7 +306,7 @@ export const splitIntoGroups = (value: string): Group[] => {
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
const lastGroupDigits = lastGroup[0].split('');
lastGroupDigits[currentPlaceInGroup] = c;
return [...acc.slice(0, -1) ?? [], [
return [...acc.slice(0, -1), [
lastGroupDigits.join(''),
currentPlace,
]];


+ 29
- 0
packages/core/test/systems/en-UK/long-count.test.ts View File

@@ -29,6 +29,35 @@ const doExpect = (
};

describe('British long count', () => {
it('returns empty string for empty input', () => {
expect(parse('')).toBe('');
});

it('formats doubly negative numbers to be positive', () => {
expect(stringify('--1')).toBe('one');
expect(stringify('----1')).toBe('one');
});

describe('error handling', () => {
it('throws an error when given fully unparseable input', () => {
expect(() => parse('foo')).toThrow();
});

it('throws an error when given partially unparseable input', () => {
expect(() => parse('one milliamilliafootillion')).toThrow();
expect(() => parse('one millia^foo-tillion')).toThrow();
expect(() => parse('one millia^0-tillion')).toThrow();
});
});

describe('options', () => {
it('formats one group per line', () => {
expect(stringify(1234567, { mergeTokensOptions: { oneGroupPerLine: true } })).toBe(`one million
two hundred thirty-four thousand
five hundred sixty-seven`);
});
});

describe('individual cases', () => {
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890';
const longNumberStringified1 = [


+ 32
- 0
packages/core/test/systems/en-UK/short-count.test.ts View File

@@ -29,6 +29,35 @@ const doExpect = (
};

describe('British short count', () => {
it('returns empty string for empty input', () => {
expect(parse('')).toBe('');
});

it('formats doubly negative numbers to be positive', () => {
expect(stringify('--1')).toBe('one');
expect(stringify('----1')).toBe('one');
});

describe('error handling', () => {
it('throws an error when given fully unparseable input', () => {
expect(() => parse('foo')).toThrow();
});

it('throws an error when given partially unparseable input', () => {
expect(() => parse('one milliamilliafootillion')).toThrow();
expect(() => parse('one millia^foo-tillion')).toThrow();
expect(() => parse('one millia^0-tillion')).toThrow();
});
});

describe('options', () => {
it('formats one group per line', () => {
expect(stringify(1234567, { mergeTokensOptions: { oneGroupPerLine: true } })).toBe(`one million
two hundred thirty-four thousand
five hundred sixty-seven`);
});
});

describe('individual cases', () => {
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890';
const longNumberStringified1 = [
@@ -48,10 +77,13 @@ describe('British short count', () => {

it.each`
value | stringified | parsedValue
${-1000000} | ${'negative one million'} | ${-1000000}
${-1000500} | ${'negative one million five hundred'} | ${-1000500}
${1000} | ${'one thousand'} | ${1000}
${10000} | ${'ten thousand'} | ${10000}
${12012} | ${'twelve thousand twelve'} | ${12012}
${12020} | ${'twelve thousand twenty'} | ${12020}
${20000} | ${'twenty thousand'} | ${20000}
${100000} | ${'one hundred thousand'} | ${100000}
${123456} | ${'one hundred twenty-three thousand four hundred fifty-six'} | ${123456}
${'1000005000000'} | ${'one trillion five million'} | ${'1.000005e+12'}


+ 31
- 0
packages/core/test/systems/en-US/short-count.test.ts View File

@@ -29,6 +29,35 @@ const doExpect = (
};

describe('American short count', () => {
it('returns empty string for empty input', () => {
expect(parse('')).toBe('');
});

it('formats doubly negative numbers to be positive', () => {
expect(stringify('--1')).toBe('one');
expect(stringify('----1')).toBe('one');
});

describe('error handling', () => {
it('throws an error when given fully unparseable input', () => {
expect(() => parse('foo')).toThrow();
});

it('throws an error when given partially unparseable input', () => {
expect(() => parse('one milliamilliafootillion')).toThrow();
expect(() => parse('one millia^foo-tillion')).toThrow();
expect(() => parse('one millia^0-tillion')).toThrow();
});
});

describe('options', () => {
it('formats one group per line', () => {
expect(stringify(1234567, { mergeTokensOptions: { oneGroupPerLine: true } })).toBe(`one million
two hundred thirty-four thousand
five hundred sixty-seven`);
});
});

describe('individual cases', () => {
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890';
const longNumberStringified1 = [
@@ -48,6 +77,8 @@ describe('American short count', () => {

it.each`
value | stringified | parsedValue
${-1000000} | ${'negative one million'} | ${-1000000}
${-1000500} | ${'negative one million five hundred'} | ${-1000500}
${1000} | ${'one thousand'} | ${1000}
${10000} | ${'ten thousand'} | ${10000}
${12012} | ${'twelve thousand twelve'} | ${12012}


Loading…
Cancel
Save