@@ -45,6 +45,20 @@ export const GROUP_DIGITS_INDEX = 0 as const; | |||||
*/ | */ | ||||
export const GROUP_PLACE_INDEX = 1 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. | * System for stringifying and parsing numbers. | ||||
*/ | */ | ||||
@@ -90,14 +104,15 @@ export interface NumberNameSystem { | |||||
* @see {NumberNameSystem.stringifyGroups} | * @see {NumberNameSystem.stringifyGroups} | ||||
* @returns Group[] The parsed groups. | * @returns Group[] The parsed groups. | ||||
*/ | */ | ||||
parseGroups: (tokens: string[]) => Group[]; | |||||
parseGroups: (tokens: string[]) => ParseResult; | |||||
/** | /** | ||||
* Combines groups into a string. | * Combines groups into a string. | ||||
* @param groups - The groups to combine. | * @param groups - The groups to combine. | ||||
* @param negative - Whether the number is negative. | |||||
* @see {NumberNameSystem.splitIntoGroups} | * @see {NumberNameSystem.splitIntoGroups} | ||||
* @returns string The combined groups in exponential form. | * @returns string The combined groups in exponential form. | ||||
*/ | */ | ||||
combineGroups: (groups: Group[]) => string; | |||||
combineGroups: (groups: Group[], negative: boolean) => string; | |||||
} | } | ||||
/** | /** | ||||
@@ -113,8 +113,8 @@ export const parse = (value: string, options = {} as ParseOptions) => { | |||||
const { system = enUS.shortCount, type = 'string' } = options; | const { system = enUS.shortCount, type = 'string' } = options; | ||||
const tokens = system.tokenize(value); | 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) { | switch (type) { | ||||
case 'number': { | case 'number': { | ||||
@@ -16,7 +16,7 @@ import { | |||||
ILLION_SUFFIX, | ILLION_SUFFIX, | ||||
MILLIA_PREFIX, | MILLIA_PREFIX, | ||||
MILLIONS_PREFIXES, | MILLIONS_PREFIXES, | ||||
MILLIONS_SPECIAL_PREFIXES, | |||||
MILLIONS_SPECIAL_PREFIXES, NEGATIVE, | |||||
NEGATIVE_SYMBOL, | NEGATIVE_SYMBOL, | ||||
ONES, | ONES, | ||||
OnesName, | OnesName, | ||||
@@ -260,6 +260,7 @@ interface ParserState { | |||||
lastToken?: string; | lastToken?: string; | ||||
groups: Group[]; | groups: Group[]; | ||||
mode: ParseGroupsMode; | mode: ParseGroupsMode; | ||||
negative: boolean; | |||||
} | } | ||||
const parseThousand = (acc: ParserState, token: string): ParserState => { | const parseThousand = (acc: ParserState, token: string): ParserState => { | ||||
@@ -404,7 +405,7 @@ const parseTens = (acc: ParserState, token: string): ParserState => { | |||||
export const parseGroups = (tokens: string[]) => { | export const parseGroups = (tokens: string[]) => { | ||||
// We add a final token which is an empty string to parse whatever the last non-empty token is. | // 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 tokensToParse = [...tokens, FINAL_TOKEN]; | ||||
const { groups } = tokensToParse.reduce<ParserState>( | |||||
const { groups, negative } = tokensToParse.reduce<ParserState>( | |||||
(acc, token) => { | (acc, token) => { | ||||
if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) { | if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) { | ||||
return parseThousand(acc, token); | return parseThousand(acc, token); | ||||
@@ -430,6 +431,13 @@ export const parseGroups = (tokens: string[]) => { | |||||
return parseTens(acc, token); | return parseTens(acc, token); | ||||
} | } | ||||
if (token === NEGATIVE) { | |||||
return { | |||||
...acc, | |||||
negative: !acc.negative, | |||||
}; | |||||
} | |||||
return { | return { | ||||
...acc, | ...acc, | ||||
lastToken: token, | lastToken: token, | ||||
@@ -439,23 +447,25 @@ export const parseGroups = (tokens: string[]) => { | |||||
lastToken: undefined, | lastToken: undefined, | ||||
groups: [], | groups: [], | ||||
mode: ParseGroupsMode.INITIAL, | mode: ParseGroupsMode.INITIAL, | ||||
negative: false, | |||||
}, | }, | ||||
); | ); | ||||
return groups; | |||||
return { groups, negative }; | |||||
}; | }; | ||||
/** | /** | ||||
* Combines groups into a string. | * Combines groups into a string. | ||||
* @param groups - The groups to combine. | * @param groups - The groups to combine. | ||||
* @param negative - Whether the number is negative. | |||||
* @see {NumberNameSystem.splitIntoGroups} | * @see {NumberNameSystem.splitIntoGroups} | ||||
* @returns string The combined groups in exponential form. | * @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 ''; | return ''; | ||||
} | } | ||||
const places = groups.map((g) => g[GROUP_PLACE_INDEX]); | |||||
const maxPlace = bigIntMax(...places) as bigint; | const maxPlace = bigIntMax(...places) as bigint; | ||||
const minPlace = bigIntMin(...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) ?? [...EMPTY_PLACE]; | ||||
@@ -486,7 +496,7 @@ export const combineGroups = (groups: Group[]) => { | |||||
const significandInteger = digits.slice(0, 1); | const significandInteger = digits.slice(0, 1); | ||||
const significandFraction = digits.slice(1).replace(/0+$/, ''); | const significandFraction = digits.slice(1).replace(/0+$/, ''); | ||||
if (significandFraction.length > 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}`; | |||||
}; | }; |
@@ -16,7 +16,7 @@ import { | |||||
ILLION_SUFFIX, | ILLION_SUFFIX, | ||||
MILLIA_PREFIX, | MILLIA_PREFIX, | ||||
MILLIONS_PREFIXES, | MILLIONS_PREFIXES, | ||||
MILLIONS_SPECIAL_PREFIXES, | |||||
MILLIONS_SPECIAL_PREFIXES, NEGATIVE, | |||||
NEGATIVE_SYMBOL, | NEGATIVE_SYMBOL, | ||||
ONES, | ONES, | ||||
OnesName, | OnesName, | ||||
@@ -47,7 +47,7 @@ export const tokenize = (value: string) => ( | |||||
.replace(/\n+/gs, ' ') | .replace(/\n+/gs, ' ') | ||||
.replace(/\s+/g, ' ') | .replace(/\s+/g, ' ') | ||||
.replace( | .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}`, | (_substring, milliaCount: string) => `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${milliaCount}`, | ||||
) | ) | ||||
.replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ') | .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 | * Deconstructs a group name token (e.g. "million", "duodecillion", etc.) to its affixes and | ||||
* parses them. | * parses them. | ||||
* @param result - The current state of the parser. | * @param result - The current state of the parser. | ||||
* @param originalGroupName - The original group name token. | |||||
* @returns DoParseState The next state of the parser. | * @returns DoParseState The next state of the parser. | ||||
*/ | */ | ||||
const doParseGroupName = (result: DoParseState): DoParseState => { | |||||
const doParseGroupName = (result: DoParseState, originalGroupName: string): DoParseState => { | |||||
if ( | if ( | ||||
result.groupNameCurrent.length < 1 | result.groupNameCurrent.length < 1 | ||||
@@ -151,10 +152,13 @@ const doParseGroupName = (result: DoParseState): DoParseState => { | |||||
const matchedMilliaArray = result.groupNameCurrent | const matchedMilliaArray = result.groupNameCurrent | ||||
.match(new RegExp(`^${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(\\d+)`)); | .match(new RegExp(`^${MILLIA_PREFIX}\\${SHORT_MILLIA_DELIMITER}(\\d+)`)); | ||||
if (!matchedMilliaArray) { | if (!matchedMilliaArray) { | ||||
throw new InvalidTokenError(result.groupNameCurrent); | |||||
throw new InvalidTokenError(originalGroupName); | |||||
} | } | ||||
const [wholeMilliaPrefix, matchedMillia] = matchedMilliaArray; | const [wholeMilliaPrefix, matchedMillia] = matchedMilliaArray; | ||||
newMillia = Number(matchedMillia); | newMillia = Number(matchedMillia); | ||||
if (newMillia < 1) { | |||||
throw new InvalidTokenError(originalGroupName); | |||||
} | |||||
prefix = wholeMilliaPrefix; | prefix = wholeMilliaPrefix; | ||||
} else { | } else { | ||||
newMillia = result.milliaIndex + 1; | 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 { | do { | ||||
result = doParseGroupName(result); | |||||
result = doParseGroupName(result, groupName); | |||||
} while (!result.done); | } while (!result.done); | ||||
const bigGroupPlace = BigInt( | const bigGroupPlace = BigInt( | ||||
@@ -254,6 +258,7 @@ interface ParserState { | |||||
lastToken?: string; | lastToken?: string; | ||||
groups: Group[]; | groups: Group[]; | ||||
mode: ParseGroupsMode; | mode: ParseGroupsMode; | ||||
negative: boolean; | |||||
} | } | ||||
const parseThousand = (acc: ParserState, token: string): ParserState => { | const parseThousand = (acc: ParserState, token: string): ParserState => { | ||||
@@ -398,7 +403,7 @@ const parseTens = (acc: ParserState, token: string): ParserState => { | |||||
export const parseGroups = (tokens: string[]) => { | export const parseGroups = (tokens: string[]) => { | ||||
// We add a final token which is an empty string to parse whatever the last non-empty token is. | // 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 tokensToParse = [...tokens, FINAL_TOKEN]; | ||||
const { groups } = tokensToParse.reduce<ParserState>( | |||||
const { groups, negative } = tokensToParse.reduce<ParserState>( | |||||
(acc, token) => { | (acc, token) => { | ||||
if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) { | if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) { | ||||
return parseThousand(acc, token); | return parseThousand(acc, token); | ||||
@@ -424,35 +429,41 @@ export const parseGroups = (tokens: string[]) => { | |||||
return parseTens(acc, token); | return parseTens(acc, token); | ||||
} | } | ||||
return { | |||||
...acc, | |||||
lastToken: token, | |||||
}; | |||||
if (token === NEGATIVE) { | |||||
return { | |||||
...acc, | |||||
negative: !acc.negative, | |||||
}; | |||||
} | |||||
throw new InvalidTokenError(token); | |||||
}, | }, | ||||
{ | { | ||||
lastToken: undefined, | lastToken: undefined, | ||||
groups: [], | groups: [], | ||||
mode: ParseGroupsMode.INITIAL, | mode: ParseGroupsMode.INITIAL, | ||||
negative: false, | |||||
}, | }, | ||||
); | ); | ||||
return groups; | |||||
return { groups, negative }; | |||||
}; | }; | ||||
/** | /** | ||||
* Combines groups into a string. | * Combines groups into a string. | ||||
* @param groups - The groups to combine. | * @param groups - The groups to combine. | ||||
* @param negative - Whether the number is negative. | |||||
* @see {NumberNameSystem.splitIntoGroups} | * @see {NumberNameSystem.splitIntoGroups} | ||||
* @returns string The combined groups in exponential form. | * @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 ''; | return ''; | ||||
} | } | ||||
const places = groups.map((g) => g[GROUP_PLACE_INDEX]); | |||||
const maxPlace = bigIntMax(...places) as bigint; | const maxPlace = bigIntMax(...places) as bigint; | ||||
const minPlace = bigIntMin(...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 firstGroupPlace = firstGroup[GROUP_PLACE_INDEX]; | ||||
const groupsSorted = []; | const groupsSorted = []; | ||||
for (let i = maxPlace; i >= minPlace; i = BigInt(i) - BigInt(1)) { | 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 significandInteger = digits.slice(0, 1); | ||||
const significandFraction = digits.slice(1).replace(/0+$/, ''); | const significandFraction = digits.slice(1).replace(/0+$/, ''); | ||||
if (significandFraction.length > 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}`; | |||||
}; | }; |
@@ -306,7 +306,7 @@ export const splitIntoGroups = (value: string): Group[] => { | |||||
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) { | if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) { | ||||
const lastGroupDigits = lastGroup[0].split(''); | const lastGroupDigits = lastGroup[0].split(''); | ||||
lastGroupDigits[currentPlaceInGroup] = c; | lastGroupDigits[currentPlaceInGroup] = c; | ||||
return [...acc.slice(0, -1) ?? [], [ | |||||
return [...acc.slice(0, -1), [ | |||||
lastGroupDigits.join(''), | lastGroupDigits.join(''), | ||||
currentPlace, | currentPlace, | ||||
]]; | ]]; | ||||
@@ -29,6 +29,35 @@ const doExpect = ( | |||||
}; | }; | ||||
describe('British long count', () => { | 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', () => { | describe('individual cases', () => { | ||||
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890'; | const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890'; | ||||
const longNumberStringified1 = [ | const longNumberStringified1 = [ | ||||
@@ -29,6 +29,35 @@ const doExpect = ( | |||||
}; | }; | ||||
describe('British short count', () => { | 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', () => { | describe('individual cases', () => { | ||||
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890'; | const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890'; | ||||
const longNumberStringified1 = [ | const longNumberStringified1 = [ | ||||
@@ -48,10 +77,13 @@ describe('British short count', () => { | |||||
it.each` | it.each` | ||||
value | stringified | parsedValue | value | stringified | parsedValue | ||||
${-1000000} | ${'negative one million'} | ${-1000000} | |||||
${-1000500} | ${'negative one million five hundred'} | ${-1000500} | |||||
${1000} | ${'one thousand'} | ${1000} | ${1000} | ${'one thousand'} | ${1000} | ||||
${10000} | ${'ten thousand'} | ${10000} | ${10000} | ${'ten thousand'} | ${10000} | ||||
${12012} | ${'twelve thousand twelve'} | ${12012} | ${12012} | ${'twelve thousand twelve'} | ${12012} | ||||
${12020} | ${'twelve thousand twenty'} | ${12020} | ${12020} | ${'twelve thousand twenty'} | ${12020} | ||||
${20000} | ${'twenty thousand'} | ${20000} | |||||
${100000} | ${'one hundred thousand'} | ${100000} | ${100000} | ${'one hundred thousand'} | ${100000} | ||||
${123456} | ${'one hundred twenty-three thousand four hundred fifty-six'} | ${123456} | ${123456} | ${'one hundred twenty-three thousand four hundred fifty-six'} | ${123456} | ||||
${'1000005000000'} | ${'one trillion five million'} | ${'1.000005e+12'} | ${'1000005000000'} | ${'one trillion five million'} | ${'1.000005e+12'} | ||||
@@ -29,6 +29,35 @@ const doExpect = ( | |||||
}; | }; | ||||
describe('American short count', () => { | 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', () => { | describe('individual cases', () => { | ||||
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890'; | const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890'; | ||||
const longNumberStringified1 = [ | const longNumberStringified1 = [ | ||||
@@ -48,6 +77,8 @@ describe('American short count', () => { | |||||
it.each` | it.each` | ||||
value | stringified | parsedValue | value | stringified | parsedValue | ||||
${-1000000} | ${'negative one million'} | ${-1000000} | |||||
${-1000500} | ${'negative one million five hundred'} | ${-1000500} | |||||
${1000} | ${'one thousand'} | ${1000} | ${1000} | ${'one thousand'} | ${1000} | ||||
${10000} | ${'ten thousand'} | ${10000} | ${10000} | ${'ten thousand'} | ${10000} | ||||
${12012} | ${'twelve thousand twelve'} | ${12012} | ${12012} | ${'twelve thousand twelve'} | ${12012} | ||||