Browse Source

Implement long count

Traditional British long count is available for systems.
master
TheoryOfNekomata 1 year ago
parent
commit
a05bed5908
14 changed files with 1966 additions and 413 deletions
  1. +20
    -0
      packages/core/src/common.ts
  2. +1
    -1
      packages/core/src/converter.ts
  3. +1
    -0
      packages/core/src/systems/en-UK/index.ts
  4. +2
    -0
      packages/core/src/systems/en-UK/long-count/index.ts
  5. +492
    -0
      packages/core/src/systems/en-UK/long-count/parse.ts
  6. +345
    -0
      packages/core/src/systems/en-UK/long-count/stringify.ts
  7. +2
    -21
      packages/core/src/systems/en-US/short-count/parse.ts
  8. +5
    -1
      packages/core/src/systems/en-US/short-count/stringify.ts
  9. +0
    -0
      packages/core/src/systems/en/common.ts
  10. +86
    -86
      packages/core/test/chongo.test.ts
  11. +343
    -0
      packages/core/test/systems/en-UK/long-count.test.ts
  12. +334
    -0
      packages/core/test/systems/en-UK/short-count.test.ts
  13. +0
    -304
      packages/core/test/systems/en-US.test.ts
  14. +335
    -0
      packages/core/test/systems/en-US/short-count.test.ts

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

@@ -108,3 +108,23 @@ export class InvalidTokenError extends Error {
super(`Invalid token: ${token}`);
}
}

export const bigIntMax = (...b: bigint[]) => b.reduce(
(previousMax, current) => {
if (typeof previousMax === 'undefined') {
return current;
}
return previousMax > current ? previousMax : current;
},
undefined as bigint | undefined,
);

export const bigIntMin = (...b: bigint[]) => b.reduce(
(previousMin, current) => {
if (typeof previousMin === 'undefined') {
return current;
}
return previousMin < current ? previousMin : current;
},
undefined as bigint | undefined,
);

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

@@ -15,7 +15,7 @@ const EXPONENT_DELIMITER = 'e' as const;
/**
* Allowed value type for {@link stringify}.
*/
type AllowedValue = string | number | bigint;
export type AllowedValue = string | number | bigint;

/**
* Array of allowed types for {@link parse}.


+ 1
- 0
packages/core/src/systems/en-UK/index.ts View File

@@ -1 +1,2 @@
export * as shortCount from '../en-US/short-count';
export * as longCount from './long-count';

+ 2
- 0
packages/core/src/systems/en-UK/long-count/index.ts View File

@@ -0,0 +1,2 @@
export * from './parse';
export * from './stringify';

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

@@ -0,0 +1,492 @@
import {
bigIntMax, bigIntMin,
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,
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;
}

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] = 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)}`;
}
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}`, lastGroup[GROUP_PLACE_INDEX] - BigInt(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] - BigInt(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],
};
};

/**
* 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 } = 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;
};

/**
* Combines groups into a string.
* @param groups - The groups to combine.
* @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) {
return '';
}
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 `${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
}
return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
};

+ 345
- 0
packages/core/src/systems/en-UK/long-count/stringify.ts View File

@@ -0,0 +1,345 @@
import {
Group,
GROUP_DIGITS_INDEX,
GROUP_PLACE_INDEX,
GroupPlace,
} from '../../../common';
import { numberToExponential } from '../../../exponent';
import {
CENTILLIONS_PREFIXES,
CentillionsPrefix,
DECILLIONS_PREFIXES,
DecillionsPrefix,
DECIMAL_POINT,
EMPTY_GROUP_DIGITS,
EXPONENT_DELIMITER, GROUP_SEPARATOR,
GROUPING_SYMBOL,
HUNDRED,
ILLION_SUFFIX,
MILLIA_PREFIX,
MILLIONS_PREFIXES,
MILLIONS_SPECIAL_PREFIXES,
MillionsPrefix,
MillionsSpecialPrefix,
NEGATIVE,
ONES,
OnesName,
SHORT_MILLIA_DELIMITER, SHORT_MILLIA_ILLION_DELIMITER, T_AFFIX,
TEN_PLUS_ONES,
TenPlusOnesName,
TENS,
TENS_ONES_SEPARATOR,
TensName,
THOUSAND,
} from '../../en/common';

/**
* Builds a name for numbers in tens and ones.
* @param tens - Tens digit.
* @param ones - Ones digit.
* @param addTensDashes - Whether to add dashes between the tens and ones.
* @returns string The name for the number.
*/
const makeTensName = (tens: number, ones: number, addTensDashes: boolean) => {
if (tens === 0) {
return ONES[ones];
}

if (tens === 1) {
return TEN_PLUS_ONES[ones] as unknown as TenPlusOnesName;
}

if (ones === 0) {
return TENS[tens];
}

return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${addTensDashes ? TENS_ONES_SEPARATOR : ' '}${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
};

/**
* Builds a name for numbers in hundreds, tens, and ones.
* @param hundreds - Hundreds digit.
* @param tens - Tens digit.
* @param ones - Ones digit.
* @param addTensDashes - Whether to add dashes between the tens and ones.
* @returns string The name for the number.
*/
const makeHundredsName = (hundreds: number, tens: number, ones: number, addTensDashes: boolean) => {
if (hundreds === 0) {
return makeTensName(tens, ones, addTensDashes);
}

if (tens === 0 && ones === 0) {
return `${ONES[hundreds]} ${HUNDRED}` as const;
}

return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones, addTensDashes)}` as const;
};

/**
* Builds a name for numbers in the millions.
* @param millions - Millions digit.
* @param currentMillia - Current millia- group.
* @param longestMilliaCount - Number of millia- groups.
* @returns string The millions prefix.
*/
const makeMillionsPrefix = (
millions: number,
currentMillia: GroupPlace,
longestMilliaCount: GroupPlace,
) => {
if (currentMillia === BigInt(0) && longestMilliaCount === BigInt(0)) {
return MILLIONS_SPECIAL_PREFIXES[millions] as MillionsSpecialPrefix;
}

return MILLIONS_PREFIXES[millions] as MillionsPrefix;
};

/**
* Builds a name for numbers in the decillions.
* @param decillions - Decillions digit.
* @param millions - Millions digit.
* @param currentMillia - Current millia- group.
* @param longestMilliaCount - Number of millia- groups.
* @returns string The decillions prefix.
*/
const makeDecillionsPrefix = (
decillions: number,
millions: number,
currentMillia: GroupPlace,
longestMilliaCount: GroupPlace,
) => {
if (decillions === 0) {
return makeMillionsPrefix(millions, currentMillia, longestMilliaCount);
}

const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
return `${onesPrefix}${tensName}` as const;
};

/**
* Builds a name for numbers in the centillions.
* @param centillions - Centillions digit.
* @param decillions - Decillions digit.
* @param millions - Millions digit.
* @param currentMillia - Current millia- group.
* @param longestMilliaCount - Number of millia- groups.
* @returns string The centillions prefix.
*/
const makeCentillionsPrefix = (
centillions: number,
decillions: number,
millions: number,
currentMillia: GroupPlace,
longestMilliaCount: GroupPlace,
) => {
if (centillions === 0) {
return makeDecillionsPrefix(decillions, millions, currentMillia, longestMilliaCount);
}

const onesPrefix = MILLIONS_PREFIXES[millions] as MillionsPrefix;
const tensName = DECILLIONS_PREFIXES[decillions] as DecillionsPrefix;
const hundredsName = CENTILLIONS_PREFIXES[centillions] as CentillionsPrefix;
return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const;
};

/**
* Repeats a string a given number of times.
* @param s - String to repeat.
* @param count - Number of times to repeat the string.
* @returns string The repeated string.
*/
const repeatString = (s: string, count: GroupPlace) => {
let result = '';
for (let i = BigInt(0); i < count; i += BigInt(1)) {
result += s;
}
return result;
};

const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
if (place === BigInt(0)) {
return '' as const;
}

if (place === BigInt(1)) {
return THOUSAND;
}

const isThousand = place % BigInt(2) === BigInt(1);

const bigGroupPlace = place / BigInt(2);
const groupGroups = bigGroupPlace
.toString()
.split('')
.reduceRight<Group[]>(
(acc, c, i, cc) => {
const firstGroup = acc.at(0);
const currentPlace = BigInt(Math.floor((cc.length - i - 1) / 3));
const newGroup = [EMPTY_GROUP_DIGITS, currentPlace] as Group;
if (typeof firstGroup === 'undefined') {
newGroup[GROUP_DIGITS_INDEX] = c;
return [newGroup];
}

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

newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
return [
newGroup,
...acc.slice(1),
];
},
[],
)
.map(([groupDigits, groupPlace]) => [groupDigits.padStart(3, '0'), groupPlace] as const)
.filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
.map(([groupDigits, groupPlace], _index, millias) => {
const [hundreds, tens, ones] = groupDigits.split('').map(Number);
const largestMillia = millias[0][GROUP_PLACE_INDEX];

const centillionsPrefix = makeCentillionsPrefix(
hundreds,
tens,
ones,
groupPlace,
largestMillia,
);

if (groupPlace < 1) {
return centillionsPrefix;
}

const milliaSuffix = (
shortenMillia && groupPlace > 1
? `${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}${groupPlace}${SHORT_MILLIA_ILLION_DELIMITER}`
: repeatString(MILLIA_PREFIX, groupPlace)
);

if (groupDigits === '001' && groupPlace === largestMillia) {
return milliaSuffix;
}

return `${centillionsPrefix}${milliaSuffix}`;
})
.join('');

if (groupGroups.endsWith(DECILLIONS_PREFIXES[1])) {
return `${isThousand ? `${THOUSAND} ` : ''}${groupGroups}${ILLION_SUFFIX}` as const;
}

if (bigGroupPlace > 10) {
// vigin - t - illion, cen - t - illion, etc.
return `${isThousand ? `${THOUSAND} ` : ''}${groupGroups}${T_AFFIX}${ILLION_SUFFIX}` as const;
}

return `${isThousand ? `${THOUSAND} ` : ''}${groupGroups}${ILLION_SUFFIX}` as const;
};

export interface StringifyGroupsOptions {
/**
* Whether to add dashes between tens and ones (e.g. "sixty-nine").
*/
addTensDashes?: boolean;
/**
* Use "millia^2-tillion" instead of "milliamilliatillion".
*/
shortenMillia?: boolean;
}

/**
* Creates a group string.
* @param groups - The groups.
* @param options - Options to use when creating the group.
* @returns string[] The groups represented into strings.
*/
export const stringifyGroups = (groups: Group[], options?: StringifyGroupsOptions): string[] => {
const filteredGroups = groups.filter(([digits, place]) => (
place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS
));

return filteredGroups.map(
([group, place]) => {
const makeHundredsArgs = group
.padStart(3, '0')
.split('')
.map((s) => Number(s)) as [number, number, number];

const groupDigitsName = makeHundredsName(
...makeHundredsArgs,
options?.addTensDashes ?? true,
);
const groupName = getGroupName(place, options?.shortenMillia ?? false);
if (groupName.length > 0) {
return `${groupDigitsName} ${groupName}`;
}
return groupDigitsName;
},
);
};

/**
* Group a number string into groups of three digits, starting from the decimal point.
* @param value - The number string to group.
* @returns Group[] The groups.
*/
export const splitIntoGroups = (value: string): Group[] => {
const [significand, exponentString] = numberToExponential(
value,
{
decimalPoint: DECIMAL_POINT,
groupingSymbol: GROUPING_SYMBOL,
exponentDelimiter: EXPONENT_DELIMITER,
},
)
.split(EXPONENT_DELIMITER);
// FIXME use bigint for exponent and indexing???
const exponent = Number(exponentString);
const significantDigits = significand.replace(new RegExp(`\\${DECIMAL_POINT}`, 'g'), '');
return significantDigits.split('').reduce<Group[]>(
(acc, c, i) => {
const currentPlace = BigInt(Math.floor((exponent - i) / 3));
const lastGroup = acc.at(-1) ?? [EMPTY_GROUP_DIGITS, currentPlace];
const currentPlaceInGroup = 2 - ((exponent - i) % 3);
if (lastGroup[GROUP_PLACE_INDEX] === currentPlace) {
const lastGroupDigits = lastGroup[0].split('');
lastGroupDigits[currentPlaceInGroup] = c;
return [...acc.slice(0, -1) ?? [], [
lastGroupDigits.join(''),
currentPlace,
]];
}
return [...acc, [c.padEnd(3, '0'), currentPlace]];
},
[],
);
};

export interface MergeTokensOptions {
oneGroupPerLine?: boolean;
}

/**
* Formats the final tokenized string.
* @param tokens - The tokens to finalize.
* @param options - The options to use.
*/
export const mergeTokens = (tokens: string[], options?: MergeTokensOptions) => (
tokens
.map((t) => t.trim())
.join(options?.oneGroupPerLine ? '\n' : GROUP_SEPARATOR)
.trim()
);

/**
* Makes a negative string.
* @param s - The string to make negative.
*/
export const makeNegative = (s: string) => {
const negativePrefix = `${NEGATIVE} `;
return s.startsWith(negativePrefix) ? s.slice(negativePrefix.length) : `${negativePrefix}${s}`;
};

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

@@ -1,4 +1,5 @@
import {
bigIntMax, bigIntMin,
Group,
GROUP_DIGITS_INDEX,
GROUP_PLACE_INDEX,
@@ -29,7 +30,7 @@ import {
TENS_ONES_SEPARATOR,
TensName,
THOUSAND,
} from '../common';
} from '../../en/common';

const FINAL_TOKEN = '' as const;

@@ -438,26 +439,6 @@ export const parseGroups = (tokens: string[]) => {
return groups;
};

const bigIntMax = (...b: bigint[]) => b.reduce(
(previousMax, current) => {
if (typeof previousMax === 'undefined') {
return current;
}
return previousMax > current ? previousMax : current;
},
undefined as bigint | undefined,
);

const bigIntMin = (...b: bigint[]) => b.reduce(
(previousMin, current) => {
if (typeof previousMin === 'undefined') {
return current;
}
return previousMin < current ? previousMin : current;
},
undefined as bigint | undefined,
);

/**
* Combines groups into a string.
* @param groups - The groups to combine.


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

@@ -31,7 +31,7 @@ import {
TENS_ONES_SEPARATOR,
TensName,
THOUSAND,
} from '../common';
} from '../../en/common';

/**
* Builds a name for numbers in tens and ones.
@@ -79,6 +79,7 @@ const makeHundredsName = (hundreds: number, tens: number, ones: number, addTensD
/**
* Builds a name for numbers in the millions.
* @param millions - Millions digit.
* @param currentMillia - Current millia- group.
* @param longestMilliaCount - Number of millia- groups.
* @returns string The millions prefix.
*/
@@ -98,6 +99,7 @@ const makeMillionsPrefix = (
* Builds a name for numbers in the decillions.
* @param decillions - Decillions digit.
* @param millions - Millions digit.
* @param currentMillia - Current millia- group.
* @param longestMilliaCount - Number of millia- groups.
* @returns string The decillions prefix.
*/
@@ -121,6 +123,7 @@ const makeDecillionsPrefix = (
* @param centillions - Centillions digit.
* @param decillions - Decillions digit.
* @param millions - Millions digit.
* @param currentMillia - Current millia- group.
* @param longestMilliaCount - Number of millia- groups.
* @returns string The centillions prefix.
*/
@@ -292,6 +295,7 @@ export const splitIntoGroups = (value: string): Group[] => {
},
)
.split(EXPONENT_DELIMITER);
// FIXME use bigint for exponent and indexing???
const exponent = Number(exponentString);
const significantDigits = significand.replace(new RegExp(`\\${DECIMAL_POINT}`, 'g'), '');
return significantDigits.split('').reduce<Group[]>(


packages/core/src/systems/en-US/common.ts → packages/core/src/systems/en/common.ts View File


packages/core/test/systems/en-US/chongo.test.ts → packages/core/test/chongo.test.ts View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { stringify, parse } from '../../../src';
import { numberToExponential } from '../../../src/exponent';
import { stringify, parse } from '../src';
import { numberToExponential } from '../src/exponent';

const stringifyOptions = {
stringifyGroupsOptions: {
@@ -11,17 +11,17 @@ const stringifyOptions = {
describe('Landon\'s original test cases', () => {
describe('Basic conversions', () => {
it.each`
value | americanName
${1} | ${'one'}
${1000} | ${'one thousand'}
${1000000} | ${'one million'}
${1000000000} | ${'one billion'}
${1000000000000} | ${'one trillion'}
${1000000000000000} | ${'one quadrillion'}
${1000000000000000000} | ${'one quintillion'}
`('converts $value to $americanName', ({ value, americanName }: { value: number, americanName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(americanName);
expect(parse(americanName, { type: 'number' })).toBe(value);
value | numberName
${'1e+0'} | ${'one'}
${'1e+3'} | ${'one thousand'}
${'1e+6'} | ${'one million'}
${'1e+9'} | ${'one billion'}
${'1e+12'} | ${'one trillion'}
${'1e+15'} | ${'one quadrillion'}
${'1e+18'} | ${'one quintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(numberName);
expect(parse(numberName)).toBe(value);
});

it(
@@ -85,86 +85,86 @@ describe('Landon\'s original test cases', () => {
describe('Medium size numbers (<= 1e+63)', () => {
describe('Table 1', () => {
it.each`
value | americanName
${'1e+9'} | ${'billion'}
${'1e+12'} | ${'trillion'}
${'1e+15'} | ${'quadrillion'}
${'1e+18'} | ${'quintillion'}
${'1e+21'} | ${'sextillion'}
${'1e+24'} | ${'septillion'}
${'1e+27'} | ${'octillion'}
${'1e+30'} | ${'nonillion'}
${'1e+33'} | ${'decillion'}
${'1e+36'} | ${'undecillion'}
${'1e+39'} | ${'duodecillion'}
${'1e+42'} | ${'tredecillion'}
${'1e+45'} | ${'quattuordecillion'}
${'1e+48'} | ${'quindecillion'}
${'1e+51'} | ${'sexdecillion'}
${'1e+54'} | ${'septendecillion'}
${'1e+57'} | ${'octodecillion'}
${'1e+60'} | ${'novemdecillion'}
${'1e+63'} | ${'vigintillion'}
`('converts $value to $americanName', ({ value, americanName }: { value: string, americanName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(`one ${americanName}`);
expect(parse(`one ${americanName}`)).toBe(value);
value | numberName
${'1e+9'} | ${'one billion'}
${'1e+12'} | ${'one trillion'}
${'1e+15'} | ${'one quadrillion'}
${'1e+18'} | ${'one quintillion'}
${'1e+21'} | ${'one sextillion'}
${'1e+24'} | ${'one septillion'}
${'1e+27'} | ${'one octillion'}
${'1e+30'} | ${'one nonillion'}
${'1e+33'} | ${'one decillion'}
${'1e+36'} | ${'one undecillion'}
${'1e+39'} | ${'one duodecillion'}
${'1e+42'} | ${'one tredecillion'}
${'1e+45'} | ${'one quattuordecillion'}
${'1e+48'} | ${'one quindecillion'}
${'1e+51'} | ${'one sexdecillion'}
${'1e+54'} | ${'one septendecillion'}
${'1e+57'} | ${'one octodecillion'}
${'1e+60'} | ${'one novemdecillion'}
${'1e+63'} | ${'one vigintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(numberName);
expect(parse(numberName)).toBe(value);
});
});
});

describe('Large size numbers (< 1e+303)', () => {
it.each`
value | americanName
${'1e+66'} | ${'unvigintillion'}
${'1e+69'} | ${'duovigintillion'}
${'1e+72'} | ${'trevigintillion'}
${'1e+75'} | ${'quattuorvigintillion'}
${'1e+78'} | ${'quinvigintillion'}
${'1e+81'} | ${'sexvigintillion'}
${'1e+84'} | ${'septenvigintillion'}
${'1e+87'} | ${'octovigintillion'}
${'1e+90'} | ${'novemvigintillion'}
${'1e+93'} | ${'trigintillion'}
${'1e+123'} | ${'quadragintillion'}
${'1e+150'} | ${'novemquadragintillion'}
${'1e+153'} | ${'quinquagintillion'}
${'1e+156'} | ${'unquinquagintillion'}
${'1e+183'} | ${'sexagintillion'}
${'1e+213'} | ${'septuagintillion'}
${'1e+222'} | ${'treseptuagintillion'}
${'1e+243'} | ${'octogintillion'}
${'1e+273'} | ${'nonagintillion'}
${'1e+300'} | ${'novemnonagintillion'}
`('converts $value to $americanName', ({ value, americanName }: { value: string, americanName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(`one ${americanName}`);
expect(parse(`one ${americanName}`)).toBe(value);
value | numberName
${'1e+66'} | ${'one unvigintillion'}
${'1e+69'} | ${'one duovigintillion'}
${'1e+72'} | ${'one trevigintillion'}
${'1e+75'} | ${'one quattuorvigintillion'}
${'1e+78'} | ${'one quinvigintillion'}
${'1e+81'} | ${'one sexvigintillion'}
${'1e+84'} | ${'one septenvigintillion'}
${'1e+87'} | ${'one octovigintillion'}
${'1e+90'} | ${'one novemvigintillion'}
${'1e+93'} | ${'one trigintillion'}
${'1e+123'} | ${'one quadragintillion'}
${'1e+150'} | ${'one novemquadragintillion'}
${'1e+153'} | ${'one quinquagintillion'}
${'1e+156'} | ${'one unquinquagintillion'}
${'1e+183'} | ${'one sexagintillion'}
${'1e+213'} | ${'one septuagintillion'}
${'1e+222'} | ${'one treseptuagintillion'}
${'1e+243'} | ${'one octogintillion'}
${'1e+273'} | ${'one nonagintillion'}
${'1e+300'} | ${'one novemnonagintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(numberName);
expect(parse(numberName)).toBe(value);
});
});

describe('Gigantic size numbers (< 1e+3003)', () => {
it.each`
value | americanName
${'1e+303'} | ${'centillion'}
${'1e+306'} | ${'cenuntillion'}
${'1e+309'} | ${'cenduotillion'}
${'1e+312'} | ${'centretillion'}
${'1e+315'} | ${'cenquattuortillion'}
${'1e+318'} | ${'cenquintillion'}
${'1e+321'} | ${'censextillion'}
${'1e+324'} | ${'censeptentillion'}
${'1e+327'} | ${'cenoctotillion'}
${'1e+330'} | ${'cennovemtillion'}
${'1e+603'} | ${'duocentillion'}
${'1e+903'} | ${'trecentillion'}
${'1e+1203'} | ${'quadringentillion'}
${'1e+1503'} | ${'quingentillion'}
${'1e+1803'} | ${'sescentillion'}
${'1e+2103'} | ${'septingentillion'}
${'1e+2403'} | ${'octingentillion'}
${'1e+2703'} | ${'nongentillion'}
`('converts $value to $americanName', ({ value, americanName }: { value: string, americanName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(`one ${americanName}`);
expect(parse(`one ${americanName}`)).toBe(value);
value | numberName
${'1e+303'} | ${'one centillion'}
${'1e+306'} | ${'one cenuntillion'}
${'1e+309'} | ${'one cenduotillion'}
${'1e+312'} | ${'one centretillion'}
${'1e+315'} | ${'one cenquattuortillion'}
${'1e+318'} | ${'one cenquintillion'}
${'1e+321'} | ${'one censextillion'}
${'1e+324'} | ${'one censeptentillion'}
${'1e+327'} | ${'one cenoctotillion'}
${'1e+330'} | ${'one cennovemtillion'}
${'1e+603'} | ${'one duocentillion'}
${'1e+903'} | ${'one trecentillion'}
${'1e+1203'} | ${'one quadringentillion'}
${'1e+1503'} | ${'one quingentillion'}
${'1e+1803'} | ${'one sescentillion'}
${'1e+2103'} | ${'one septingentillion'}
${'1e+2403'} | ${'one octingentillion'}
${'1e+2703'} | ${'one nongentillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(numberName);
expect(parse(numberName)).toBe(value);
});
});

@@ -183,7 +183,7 @@ describe('Landon\'s original test cases', () => {
);

it.each`
value | americanName
value | numberName
${'1.23456789246801357e+6221'} | ${'one hundred twenty three duomilliaduoseptuagintillion four hundred fifty six duomilliaunseptuagintillion seven hundred eighty nine duomilliaseptuagintillion two hundred forty six duomillianovemsexagintillion eight hundred one duomilliaoctosexagintillion three hundred fifty seven duomilliaseptensexagintillion'}
${'1.23456789246801357e+2961221'} | ${'one hundred twenty three nongenseptenoctoginmilliaduoseptuagintillion four hundred fifty six nongenseptenoctoginmilliaunseptuagintillion seven hundred eighty nine nongenseptenoctoginmilliaseptuagintillion two hundred forty six nongenseptenoctoginmillianovemsexagintillion eight hundred one nongenseptenoctoginmilliaoctosexagintillion three hundred fifty seven nongenseptenoctoginmilliaseptensexagintillion'}
${'123.456789246801357e+2961000000219'} | ${'one hundred twenty three nongenseptenoctoginmilliamilliamilliaduoseptuagintillion four hundred fifty six nongenseptenoctoginmilliamilliamilliaunseptuagintillion seven hundred eighty nine nongenseptenoctoginmilliamilliamilliaseptuagintillion two hundred forty six nongenseptenoctoginmilliamilliamillianovemsexagintillion eight hundred one nongenseptenoctoginmilliamilliamilliaoctosexagintillion three hundred fifty seven nongenseptenoctoginmilliamilliamilliaseptensexagintillion'}
@@ -195,9 +195,9 @@ describe('Landon\'s original test cases', () => {
${'1e+1174743648579'} | ${'one trecenunnonaginmilliamilliamilliaquingenunoctoginmilliamilliaduocensexdecmilliacenduononagintillion'}
${'1e+3000000000003'} | ${'one milliamilliamilliamilliatillion'}
${'1e+696276510359811'} | ${'one duocenduotriginmilliamilliamilliamilliaduononaginmilliamilliamilliacenseptuaginmilliamilliacennovemdecmillianongensextrigintillion'}
`('converts $value to $americanName', ({ value, americanName }: { value: string, americanName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(americanName);
expect(parse(americanName)).toBe(numberToExponential(value));
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
expect(stringify(value, stringifyOptions)).toBe(numberName);
expect(parse(numberName)).toBe(numberToExponential(value));
});
});
});

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

@@ -0,0 +1,343 @@
import { describe, it, expect } from 'vitest';
import {
AllowedValue,
parse,
stringify,
systems,
} from '../../../src';

const options = {
system: systems.enUK.longCount,
};

const doExpect = (
value: AllowedValue,
stringified: string,
parsedValue: AllowedValue = value,
) => {
const stringifyOptions = {
...options,
};

const parseOptions = {
...options,
type: typeof value as ('number' | 'string' | 'bigint'),
};

expect(stringify(value, stringifyOptions)).toBe(stringified);
expect(parse(stringified, parseOptions)).toBe(parsedValue);
};

describe('British long count', () => {
describe('individual cases', () => {
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890';
const longNumberStringified1 = [
'one hundred twenty-three thousand quadrillion',
'four hundred fifty-six quadrillion',
'seven hundred eighty-nine thousand trillion',
'twelve trillion',
'three hundred forty-five thousand billion',
'six hundred seventy-eight billion',
'nine hundred one thousand million',
'two hundred thirty-four million',
'five hundred sixty-seven thousand',
'eight hundred ninety',
].join(' ');

const longNumberParsedValue = '1.2345678901234567890123456789e+29';

it.each`
value | stringified | parsedValue
${1000} | ${'one thousand'} | ${1000}
${10000} | ${'ten thousand'} | ${10000}
${12012} | ${'twelve thousand twelve'} | ${12012}
${12020} | ${'twelve thousand twenty'} | ${12020}
${100000} | ${'one hundred thousand'} | ${100000}
${123456} | ${'one hundred twenty-three thousand four hundred fifty-six'} | ${123456}
${'1000005000000'} | ${'one billion five million'} | ${'1.000005e+12'}
${longNumberValue1} | ${longNumberStringified1} | ${longNumberParsedValue}
${'1e6006'} | ${'one milliauntillion'} | ${'1e+6006'}
`('converts $value to $stringified', ({
value,
stringified,
parsedValue,
}: { value: AllowedValue, stringified: string, parsedValue: AllowedValue }) => {
doExpect(value, stringified, parsedValue);
});

it('converts one millia^1-tillion to 1e+3003', () => {
expect(parse('one millia^1-tillion')).toBe('1e+3003');
});
});

describe('blanket cases', () => {
it('converts one millia^2-tillion', () => {
const value = '1e+3000003';
const stringified = 'one millia^2-tillion';
expect(parse(stringified)).toBe(value);
expect(stringify(value, { stringifyGroupsOptions: { shortenMillia: true } }))
.toBe(stringified);
});

it('converts one millia^2-unmilliatillion', () => {
const value = '1e+3003003';
const stringified = 'one millia^2-unmilliatillion';
expect(stringify(value, { stringifyGroupsOptions: { shortenMillia: true } }))
.toBe(stringified);
expect(parse(stringified)).toBe(value);
});

it('converts one milliamilliaunmilliatillion', () => {
const value = '1e+3003003';
const stringified = 'one milliamilliaunmilliatillion';
expect(stringify(value)).toBe(stringified);
expect(parse(stringified)).toBe('1e+3003003');
});
});

describe('general', () => {
describe('0-9', () => {
it.each`
value | numberName
${0} | ${'zero'}
${1} | ${'one'}
${2} | ${'two'}
${3} | ${'three'}
${4} | ${'four'}
${5} | ${'five'}
${6} | ${'six'}
${7} | ${'seven'}
${8} | ${'eight'}
${9} | ${'nine'}
`('converts $ones to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe('10-19', () => {
it.each`
value | numberName
${10} | ${'ten'}
${11} | ${'eleven'}
${12} | ${'twelve'}
${13} | ${'thirteen'}
${14} | ${'fourteen'}
${15} | ${'fifteen'}
${16} | ${'sixteen'}
${17} | ${'seventeen'}
${18} | ${'eighteen'}
${19} | ${'nineteen'}
`('converts $tenPlusOnes to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
tensStart | tensEnd | tensBase
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$tensStart-$tensEnd', ({
tensStart, tensBase,
}: { tensStart: number, tensBase: string }) => {
it.each`
value | numberName
${tensStart} | ${tensBase}
${tensStart + 1} | ${`${tensBase}-one`}
${tensStart + 2} | ${`${tensBase}-two`}
${tensStart + 3} | ${`${tensBase}-three`}
${tensStart + 4} | ${`${tensBase}-four`}
${tensStart + 5} | ${`${tensBase}-five`}
${tensStart + 6} | ${`${tensBase}-six`}
${tensStart + 7} | ${`${tensBase}-seven`}
${tensStart + 8} | ${`${tensBase}-eight`}
${tensStart + 9} | ${`${tensBase}-nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
hundredsStart | hundredsEnd | hundredsBase
${100} | ${199} | ${'one hundred'}
${200} | ${299} | ${'two hundred'}
${300} | ${399} | ${'three hundred'}
${400} | ${499} | ${'four hundred'}
${500} | ${599} | ${'five hundred'}
${600} | ${699} | ${'six hundred'}
${700} | ${799} | ${'seven hundred'}
${800} | ${899} | ${'eight hundred'}
${900} | ${999} | ${'nine hundred'}
`('$hundredsStart-$hundredsEnd', ({
hundredsStart, hundredsBase,
}: { hundredsStart: number, hundredsBase: string }) => {
describe(`${hundredsStart}-${hundredsStart + 9}`, () => {
it.each`
value | numberName
${hundredsStart} | ${hundredsBase}
${hundredsStart + 1} | ${`${hundredsBase} one`}
${hundredsStart + 2} | ${`${hundredsBase} two`}
${hundredsStart + 3} | ${`${hundredsBase} three`}
${hundredsStart + 4} | ${`${hundredsBase} four`}
${hundredsStart + 5} | ${`${hundredsBase} five`}
${hundredsStart + 6} | ${`${hundredsBase} six`}
${hundredsStart + 7} | ${`${hundredsBase} seven`}
${hundredsStart + 8} | ${`${hundredsBase} eight`}
${hundredsStart + 9} | ${`${hundredsBase} nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe(`${hundredsStart + 10}-${hundredsStart + 19}`, () => {
it.each`
value | numberName
${hundredsStart + 10} | ${`${hundredsBase} ten`}
${hundredsStart + 11} | ${`${hundredsBase} eleven`}
${hundredsStart + 12} | ${`${hundredsBase} twelve`}
${hundredsStart + 13} | ${`${hundredsBase} thirteen`}
${hundredsStart + 14} | ${`${hundredsBase} fourteen`}
${hundredsStart + 15} | ${`${hundredsBase} fifteen`}
${hundredsStart + 16} | ${`${hundredsBase} sixteen`}
${hundredsStart + 17} | ${`${hundredsBase} seventeen`}
${hundredsStart + 18} | ${`${hundredsBase} eighteen`}
${hundredsStart + 19} | ${`${hundredsBase} nineteen`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
start | end | base
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$start-$end', ({
start, base,
}: { start: number, base: string }) => {
it.each`
value | numberName
${hundredsStart + start} | ${`${hundredsBase} ${base}`}
${hundredsStart + start + 1} | ${`${hundredsBase} ${base}-one`}
${hundredsStart + start + 2} | ${`${hundredsBase} ${base}-two`}
${hundredsStart + start + 3} | ${`${hundredsBase} ${base}-three`}
${hundredsStart + start + 4} | ${`${hundredsBase} ${base}-four`}
${hundredsStart + start + 5} | ${`${hundredsBase} ${base}-five`}
${hundredsStart + start + 6} | ${`${hundredsBase} ${base}-six`}
${hundredsStart + start + 7} | ${`${hundredsBase} ${base}-seven`}
${hundredsStart + start + 8} | ${`${hundredsBase} ${base}-eight`}
${hundredsStart + start + 9} | ${`${hundredsBase} ${base}-nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});
});
});

it.each`
value | numberName
${'1e+6'} | ${'one million'}
${'1e+9'} | ${'one thousand million'}
${'1e+12'} | ${'one billion'}
${'1e+15'} | ${'one thousand billion'}
${'1e+18'} | ${'one trillion'}
${'1e+21'} | ${'one thousand trillion'}
${'1e+24'} | ${'one quadrillion'}
${'1e+27'} | ${'one thousand quadrillion'}
${'1e+30'} | ${'one quintillion'}
${'1e+33'} | ${'one thousand quintillion'}
${'1e+36'} | ${'one sextillion'}
${'1e+39'} | ${'one thousand sextillion'}
${'1e+42'} | ${'one septillion'}
${'1e+45'} | ${'one thousand septillion'}
${'1e+48'} | ${'one octillion'}
${'1e+51'} | ${'one thousand octillion'}
${'1e+54'} | ${'one nonillion'}
${'1e+57'} | ${'one thousand nonillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+60'} | ${'one decillion'}
${'1e+66'} | ${'one undecillion'}
${'1e+72'} | ${'one duodecillion'}
${'1e+78'} | ${'one tredecillion'}
${'1e+84'} | ${'one quattuordecillion'}
${'1e+90'} | ${'one quindecillion'}
${'1e+96'} | ${'one sexdecillion'}
${'1e+102'} | ${'one septendecillion'}
${'1e+108'} | ${'one octodecillion'}
${'1e+114'} | ${'one novemdecillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+120'} | ${'one vigintillion'}
${'1e+126'} | ${'one unvigintillion'}
${'1e+132'} | ${'one duovigintillion'}
${'1e+138'} | ${'one trevigintillion'}
${'1e+144'} | ${'one quattuorvigintillion'}
${'1e+150'} | ${'one quinvigintillion'}
${'1e+156'} | ${'one sexvigintillion'}
${'1e+162'} | ${'one septenvigintillion'}
${'1e+168'} | ${'one octovigintillion'}
${'1e+174'} | ${'one novemvigintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+180'} | ${'one trigintillion'}
${'1e+240'} | ${'one quadragintillion'}
${'1e+300'} | ${'one quinquagintillion'}
${'1e+360'} | ${'one sexagintillion'}
${'1e+420'} | ${'one septuagintillion'}
${'1e+480'} | ${'one octogintillion'}
${'1e+540'} | ${'one nonagintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+600'} | ${'one centillion'}
${'1e+606'} | ${'one cenuntillion'}
${'1e+612'} | ${'one cenduotillion'}
${'1e+618'} | ${'one centretillion'}
${'1e+624'} | ${'one cenquattuortillion'}
${'1e+630'} | ${'one cenquintillion'}
${'1e+636'} | ${'one censextillion'}
${'1e+642'} | ${'one censeptentillion'}
${'1e+648'} | ${'one cenoctotillion'}
${'1e+654'} | ${'one cennovemtillion'}
${'1e+1200'} | ${'one duocentillion'}
${'1e+1800'} | ${'one trecentillion'}
${'1e+2400'} | ${'one quadringentillion'}
${'1e+3000'} | ${'one quingentillion'}
${'1e+3600'} | ${'one sescentillion'}
${'1e+4200'} | ${'one septingentillion'}
${'1e+4800'} | ${'one octingentillion'}
${'1e+5400'} | ${'one nongentillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it('converts \'1e+6000\' to \'one milliatillion\'', () => {
doExpect('1e+6000', 'one milliatillion');
});
});

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

@@ -0,0 +1,334 @@
import { describe, it, expect } from 'vitest';
import {
AllowedValue,
parse,
stringify,
systems,
} from '../../../src';

const options = {
system: systems.enUK.shortCount,
};

const doExpect = (
value: AllowedValue,
stringified: string,
parsedValue: AllowedValue = value,
) => {
const stringifyOptions = {
...options,
};

const parseOptions = {
...options,
type: typeof value as ('number' | 'string' | 'bigint'),
};

expect(stringify(value, stringifyOptions)).toBe(stringified);
expect(parse(stringified, parseOptions)).toBe(parsedValue);
};

describe('British short count', () => {
describe('individual cases', () => {
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890';
const longNumberStringified1 = [
'one hundred twenty-three octillion',
'four hundred fifty-six septillion',
'seven hundred eighty-nine sextillion',
'twelve quintillion',
'three hundred forty-five quadrillion',
'six hundred seventy-eight trillion',
'nine hundred one billion',
'two hundred thirty-four million',
'five hundred sixty-seven thousand',
'eight hundred ninety',
].join(' ');

const longNumberParsedValue = '1.2345678901234567890123456789e+29';

it.each`
value | stringified | parsedValue
${1000} | ${'one thousand'} | ${1000}
${10000} | ${'ten thousand'} | ${10000}
${12012} | ${'twelve thousand twelve'} | ${12012}
${12020} | ${'twelve thousand twenty'} | ${12020}
${100000} | ${'one hundred thousand'} | ${100000}
${123456} | ${'one hundred twenty-three thousand four hundred fifty-six'} | ${123456}
${'1000005000000'} | ${'one trillion five million'} | ${'1.000005e+12'}
${longNumberValue1} | ${longNumberStringified1} | ${longNumberParsedValue}
${'1e3006'} | ${'one milliauntillion'} | ${'1e+3006'}
`('converts $value to $stringified', ({
value,
stringified,
parsedValue,
}: { value: AllowedValue, stringified: string, parsedValue: AllowedValue }) => {
doExpect(value, stringified, parsedValue);
});

it('converts one millia^1-tillion to 1e+3003', () => {
expect(parse('one millia^1-tillion')).toBe('1e+3003');
});
});

describe('blanket cases', () => {
it('converts one millia^2-tillion', () => {
const value = '1e+3000003';
const stringified = 'one millia^2-tillion';
expect(parse(stringified)).toBe(value);
expect(stringify(value, { stringifyGroupsOptions: { shortenMillia: true } }))
.toBe(stringified);
});

it('converts one millia^2-unmilliatillion', () => {
const value = '1e+3003003';
const stringified = 'one millia^2-unmilliatillion';
expect(stringify(value, { stringifyGroupsOptions: { shortenMillia: true } }))
.toBe(stringified);
expect(parse(stringified)).toBe(value);
});

it('converts one milliamilliaunmilliatillion', () => {
const value = '1e+3003003';
const stringified = 'one milliamilliaunmilliatillion';
expect(stringify(value)).toBe(stringified);
expect(parse(stringified)).toBe('1e+3003003');
});
});

describe('general', () => {
describe('0-9', () => {
it.each`
value | numberName
${0} | ${'zero'}
${1} | ${'one'}
${2} | ${'two'}
${3} | ${'three'}
${4} | ${'four'}
${5} | ${'five'}
${6} | ${'six'}
${7} | ${'seven'}
${8} | ${'eight'}
${9} | ${'nine'}
`('converts $ones to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe('10-19', () => {
it.each`
value | numberName
${10} | ${'ten'}
${11} | ${'eleven'}
${12} | ${'twelve'}
${13} | ${'thirteen'}
${14} | ${'fourteen'}
${15} | ${'fifteen'}
${16} | ${'sixteen'}
${17} | ${'seventeen'}
${18} | ${'eighteen'}
${19} | ${'nineteen'}
`('converts $tenPlusOnes to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
tensStart | tensEnd | tensBase
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$tensStart-$tensEnd', ({
tensStart, tensBase,
}: { tensStart: number, tensBase: string }) => {
it.each`
value | numberName
${tensStart} | ${tensBase}
${tensStart + 1} | ${`${tensBase}-one`}
${tensStart + 2} | ${`${tensBase}-two`}
${tensStart + 3} | ${`${tensBase}-three`}
${tensStart + 4} | ${`${tensBase}-four`}
${tensStart + 5} | ${`${tensBase}-five`}
${tensStart + 6} | ${`${tensBase}-six`}
${tensStart + 7} | ${`${tensBase}-seven`}
${tensStart + 8} | ${`${tensBase}-eight`}
${tensStart + 9} | ${`${tensBase}-nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
hundredsStart | hundredsEnd | hundredsBase
${100} | ${199} | ${'one hundred'}
${200} | ${299} | ${'two hundred'}
${300} | ${399} | ${'three hundred'}
${400} | ${499} | ${'four hundred'}
${500} | ${599} | ${'five hundred'}
${600} | ${699} | ${'six hundred'}
${700} | ${799} | ${'seven hundred'}
${800} | ${899} | ${'eight hundred'}
${900} | ${999} | ${'nine hundred'}
`('$hundredsStart-$hundredsEnd', ({
hundredsStart, hundredsBase,
}: { hundredsStart: number, hundredsBase: string }) => {
describe(`${hundredsStart}-${hundredsStart + 9}`, () => {
it.each`
value | numberName
${hundredsStart} | ${hundredsBase}
${hundredsStart + 1} | ${`${hundredsBase} one`}
${hundredsStart + 2} | ${`${hundredsBase} two`}
${hundredsStart + 3} | ${`${hundredsBase} three`}
${hundredsStart + 4} | ${`${hundredsBase} four`}
${hundredsStart + 5} | ${`${hundredsBase} five`}
${hundredsStart + 6} | ${`${hundredsBase} six`}
${hundredsStart + 7} | ${`${hundredsBase} seven`}
${hundredsStart + 8} | ${`${hundredsBase} eight`}
${hundredsStart + 9} | ${`${hundredsBase} nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe(`${hundredsStart + 10}-${hundredsStart + 19}`, () => {
it.each`
value | numberName
${hundredsStart + 10} | ${`${hundredsBase} ten`}
${hundredsStart + 11} | ${`${hundredsBase} eleven`}
${hundredsStart + 12} | ${`${hundredsBase} twelve`}
${hundredsStart + 13} | ${`${hundredsBase} thirteen`}
${hundredsStart + 14} | ${`${hundredsBase} fourteen`}
${hundredsStart + 15} | ${`${hundredsBase} fifteen`}
${hundredsStart + 16} | ${`${hundredsBase} sixteen`}
${hundredsStart + 17} | ${`${hundredsBase} seventeen`}
${hundredsStart + 18} | ${`${hundredsBase} eighteen`}
${hundredsStart + 19} | ${`${hundredsBase} nineteen`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
start | end | base
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$start-$end', ({
start, base,
}: { start: number, base: string }) => {
it.each`
value | numberName
${hundredsStart + start} | ${`${hundredsBase} ${base}`}
${hundredsStart + start + 1} | ${`${hundredsBase} ${base}-one`}
${hundredsStart + start + 2} | ${`${hundredsBase} ${base}-two`}
${hundredsStart + start + 3} | ${`${hundredsBase} ${base}-three`}
${hundredsStart + start + 4} | ${`${hundredsBase} ${base}-four`}
${hundredsStart + start + 5} | ${`${hundredsBase} ${base}-five`}
${hundredsStart + start + 6} | ${`${hundredsBase} ${base}-six`}
${hundredsStart + start + 7} | ${`${hundredsBase} ${base}-seven`}
${hundredsStart + start + 8} | ${`${hundredsBase} ${base}-eight`}
${hundredsStart + start + 9} | ${`${hundredsBase} ${base}-nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});
});
});

it.each`
value | numberName
${'1e+6'} | ${'one million'}
${'1e+9'} | ${'one billion'}
${'1e+12'} | ${'one trillion'}
${'1e+15'} | ${'one quadrillion'}
${'1e+18'} | ${'one quintillion'}
${'1e+21'} | ${'one sextillion'}
${'1e+24'} | ${'one septillion'}
${'1e+27'} | ${'one octillion'}
${'1e+30'} | ${'one nonillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+33'} | ${'one decillion'}
${'1e+36'} | ${'one undecillion'}
${'1e+39'} | ${'one duodecillion'}
${'1e+42'} | ${'one tredecillion'}
${'1e+45'} | ${'one quattuordecillion'}
${'1e+48'} | ${'one quindecillion'}
${'1e+51'} | ${'one sexdecillion'}
${'1e+54'} | ${'one septendecillion'}
${'1e+57'} | ${'one octodecillion'}
${'1e+60'} | ${'one novemdecillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+63'} | ${'one vigintillion'}
${'1e+66'} | ${'one unvigintillion'}
${'1e+69'} | ${'one duovigintillion'}
${'1e+72'} | ${'one trevigintillion'}
${'1e+75'} | ${'one quattuorvigintillion'}
${'1e+78'} | ${'one quinvigintillion'}
${'1e+81'} | ${'one sexvigintillion'}
${'1e+84'} | ${'one septenvigintillion'}
${'1e+87'} | ${'one octovigintillion'}
${'1e+90'} | ${'one novemvigintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+93'} | ${'one trigintillion'}
${'1e+123'} | ${'one quadragintillion'}
${'1e+153'} | ${'one quinquagintillion'}
${'1e+183'} | ${'one sexagintillion'}
${'1e+213'} | ${'one septuagintillion'}
${'1e+243'} | ${'one octogintillion'}
${'1e+273'} | ${'one nonagintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+303'} | ${'one centillion'}
${'1e+306'} | ${'one cenuntillion'}
${'1e+309'} | ${'one cenduotillion'}
${'1e+312'} | ${'one centretillion'}
${'1e+315'} | ${'one cenquattuortillion'}
${'1e+318'} | ${'one cenquintillion'}
${'1e+321'} | ${'one censextillion'}
${'1e+324'} | ${'one censeptentillion'}
${'1e+327'} | ${'one cenoctotillion'}
${'1e+330'} | ${'one cennovemtillion'}
${'1e+603'} | ${'one duocentillion'}
${'1e+903'} | ${'one trecentillion'}
${'1e+1203'} | ${'one quadringentillion'}
${'1e+1503'} | ${'one quingentillion'}
${'1e+1803'} | ${'one sescentillion'}
${'1e+2103'} | ${'one septingentillion'}
${'1e+2403'} | ${'one octingentillion'}
${'1e+2703'} | ${'one nongentillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it('converts \'1e+3003\' to \'one milliatillion\'', () => {
doExpect('1e+3003', 'one milliatillion');
});
});

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

@@ -1,304 +0,0 @@
import { describe, it, expect } from 'vitest';
import { parse, stringify, systems } from '../../src';
import { numberToExponential } from '../../src/exponent';

const options = {
system: systems.enUS.shortCount,
};

const stringifyOptions = {
...options,
stringifyGroupsOptions: {
addTensDashes: false,
},
};

describe('numerica', () => {
describe('group names', () => {
describe('0-9', () => {
it.each`
ones | expected
${0} | ${'zero'}
${1} | ${'one'}
${2} | ${'two'}
${3} | ${'three'}
${4} | ${'four'}
${5} | ${'five'}
${6} | ${'six'}
${7} | ${'seven'}
${8} | ${'eight'}
${9} | ${'nine'}
`('converts $ones to $expected', ({ ones, expected }: { ones: number, expected: string }) => {
expect(stringify(ones, stringifyOptions)).toBe(expected);
expect(parse(expected, { ...options, type: 'number' })).toBe(ones);
});
});

describe('10-19', () => {
it.each`
tenPlusOnes | expected
${10} | ${'ten'}
${11} | ${'eleven'}
${12} | ${'twelve'}
${13} | ${'thirteen'}
${14} | ${'fourteen'}
${15} | ${'fifteen'}
${16} | ${'sixteen'}
${17} | ${'seventeen'}
${18} | ${'eighteen'}
${19} | ${'nineteen'}
`('converts $tenPlusOnes to $expected', ({ tenPlusOnes, expected }: { tenPlusOnes: number, expected: string }) => {
expect(stringify(tenPlusOnes, stringifyOptions)).toBe(expected);
expect(parse(expected, { ...options, type: 'number' })).toBe(tenPlusOnes);
});
});

describe.each`
tensStart | tensEnd | tensBase
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$tensStart-$tensEnd', ({
tensStart, tensBase,
}: { tensStart: number, tensBase: string }) => {
it.each`
value | expected
${tensStart} | ${tensBase}
${tensStart + 1} | ${`${tensBase} one`}
${tensStart + 2} | ${`${tensBase} two`}
${tensStart + 3} | ${`${tensBase} three`}
${tensStart + 4} | ${`${tensBase} four`}
${tensStart + 5} | ${`${tensBase} five`}
${tensStart + 6} | ${`${tensBase} six`}
${tensStart + 7} | ${`${tensBase} seven`}
${tensStart + 8} | ${`${tensBase} eight`}
${tensStart + 9} | ${`${tensBase} nine`}
`('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, { ...options, type: 'number' })).toBe(value);
});
});

describe.each`
hundredsStart | hundredsEnd | hundredsBase
${100} | ${199} | ${'one hundred'}
${200} | ${299} | ${'two hundred'}
${300} | ${399} | ${'three hundred'}
${400} | ${499} | ${'four hundred'}
${500} | ${599} | ${'five hundred'}
${600} | ${699} | ${'six hundred'}
${700} | ${799} | ${'seven hundred'}
${800} | ${899} | ${'eight hundred'}
${900} | ${999} | ${'nine hundred'}
`('$hundredsStart-$hundredsEnd', ({
hundredsStart, hundredsBase,
}: { hundredsStart: number, hundredsBase: string }) => {
describe(`${hundredsStart}-${hundredsStart + 9}`, () => {
it.each`
value | expected
${hundredsStart} | ${hundredsBase}
${hundredsStart + 1} | ${`${hundredsBase} one`}
${hundredsStart + 2} | ${`${hundredsBase} two`}
${hundredsStart + 3} | ${`${hundredsBase} three`}
${hundredsStart + 4} | ${`${hundredsBase} four`}
${hundredsStart + 5} | ${`${hundredsBase} five`}
${hundredsStart + 6} | ${`${hundredsBase} six`}
${hundredsStart + 7} | ${`${hundredsBase} seven`}
${hundredsStart + 8} | ${`${hundredsBase} eight`}
${hundredsStart + 9} | ${`${hundredsBase} nine`}
`('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, { ...options, type: 'number' })).toBe(value);
});
});

describe(`${hundredsStart + 10}-${hundredsStart + 19}`, () => {
it.each`
value | expected
${hundredsStart + 10} | ${`${hundredsBase} ten`}
${hundredsStart + 11} | ${`${hundredsBase} eleven`}
${hundredsStart + 12} | ${`${hundredsBase} twelve`}
${hundredsStart + 13} | ${`${hundredsBase} thirteen`}
${hundredsStart + 14} | ${`${hundredsBase} fourteen`}
${hundredsStart + 15} | ${`${hundredsBase} fifteen`}
${hundredsStart + 16} | ${`${hundredsBase} sixteen`}
${hundredsStart + 17} | ${`${hundredsBase} seventeen`}
${hundredsStart + 18} | ${`${hundredsBase} eighteen`}
${hundredsStart + 19} | ${`${hundredsBase} nineteen`}
`('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, { ...options, type: 'number' })).toBe(value);
});
});

describe.each`
start | end | base
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$start-$end', ({
start, base,
}: { start: number, base: string }) => {
it.each`
value | expected
${hundredsStart + start} | ${`${hundredsBase} ${base}`}
${hundredsStart + start + 1} | ${`${hundredsBase} ${base} one`}
${hundredsStart + start + 2} | ${`${hundredsBase} ${base} two`}
${hundredsStart + start + 3} | ${`${hundredsBase} ${base} three`}
${hundredsStart + start + 4} | ${`${hundredsBase} ${base} four`}
${hundredsStart + start + 5} | ${`${hundredsBase} ${base} five`}
${hundredsStart + start + 6} | ${`${hundredsBase} ${base} six`}
${hundredsStart + start + 7} | ${`${hundredsBase} ${base} seven`}
${hundredsStart + start + 8} | ${`${hundredsBase} ${base} eight`}
${hundredsStart + start + 9} | ${`${hundredsBase} ${base} nine`}
`('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, { ...options, type: 'number' })).toBe(value);
});
});
});
});

it('converts 1000 to one thousand', () => {
expect(stringify(1000, stringifyOptions)).toBe('one thousand');
expect(parse('one thousand', { ...options, type: 'number' })).toBe(1000);
});

it('converts 10000 to ten thousand', () => {
expect(stringify(10000, stringifyOptions)).toBe('ten thousand');
expect(parse('ten thousand', { ...options, type: 'number' })).toBe(10000);
});

it('converts 100000 to one hundred thousand', () => {
expect(stringify(100000, stringifyOptions)).toBe('one hundred thousand');
expect(parse('one hundred thousand', { ...options, type: 'number' })).toBe(100000);
});

it('converts 123456 to one hundred twenty three thousand four hundred fifty six', () => {
expect(stringify(123456, stringifyOptions)).toBe('one hundred twenty three thousand four hundred fifty six');
expect(parse('one hundred twenty three thousand four hundred fifty six', { ...options, type: 'number' })).toBe(123456);
});

it.each`
value | expected
${1e+6} | ${'one million'}
${1e+9} | ${'one billion'}
${1e+12} | ${'one trillion'}
${1e+15} | ${'one quadrillion'}
${1e+18} | ${'one quintillion'}
${1e+21} | ${'one sextillion'}
${1e+24} | ${'one septillion'}
${1e+27} | ${'one octillion'}
${1e+30} | ${'one nonillion'}
`('converts $value to $expected', ({ value, expected }: { value: number, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, { ...options, type: 'number' })).toBe(value);
});

it.each`
value | expected
${'1e+33'} | ${'one decillion'}
${'1e+36'} | ${'one undecillion'}
${'1e+39'} | ${'one duodecillion'}
${'1e+42'} | ${'one tredecillion'}
${'1e+45'} | ${'one quattuordecillion'}
${'1e+48'} | ${'one quindecillion'}
${'1e+51'} | ${'one sexdecillion'}
${'1e+54'} | ${'one septendecillion'}
${'1e+57'} | ${'one octodecillion'}
${'1e+60'} | ${'one novemdecillion'}
`('converts $value to $expected', ({ value, expected }: { value: string, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, options)).toBe(value);
});

it.each`
value | expected
${'1e+63'} | ${'one vigintillion'}
${'1e+66'} | ${'one unvigintillion'}
${'1e+69'} | ${'one duovigintillion'}
${'1e+72'} | ${'one trevigintillion'}
${'1e+75'} | ${'one quattuorvigintillion'}
${'1e+78'} | ${'one quinvigintillion'}
${'1e+81'} | ${'one sexvigintillion'}
${'1e+84'} | ${'one septenvigintillion'}
${'1e+87'} | ${'one octovigintillion'}
${'1e+90'} | ${'one novemvigintillion'}
`('converts $value to $expected', ({ value, expected }: { value: string, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, options)).toBe(value);
});

it.each`
value | expected
${'1e+93'} | ${'one trigintillion'}
${'1e+123'} | ${'one quadragintillion'}
${'1e+153'} | ${'one quinquagintillion'}
${'1e+183'} | ${'one sexagintillion'}
${'1e+213'} | ${'one septuagintillion'}
${'1e+243'} | ${'one octogintillion'}
${'1e+273'} | ${'one nonagintillion'}
`('converts $value to $expected', ({ value, expected }: { value: string, expected: string }) => {
expect(stringify(value, stringifyOptions)).toBe(expected);
expect(parse(expected, options)).toBe(value);
});

it('converts values', () => {
const exp1 = numberToExponential('123,456,789,012,345,678,901,234,567,890');
//
// one hundred twenty three octillion
// four hundred fifty six septillion
// seven hundred eighty nine sextillion
// twelve quintillion
// three hundred forty five quadrillion
// six hundred seventy eight trillion
// nine hundred one billion
// two hundred thirty four million
// five hundred sixty seven thousand
// eight hundred ninety
expect(parse(stringify('123456789012345678901234567890'))).toBe(exp1);

const value2 = '1000005000000';
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);

const value5 = '1e3006';
expect(stringify(value5)).toBe('one milliauntillion');
});

it('converts short millia', () => {
const shortMillia1 = 'one millia^1-tillion';
expect(parse(shortMillia1)).toBe('1e+3003');

const shortMillia2 = 'one millia^2-tillion';
expect(parse(shortMillia2)).toBe('1e+3000003');
expect(stringify(parse(shortMillia2), { stringifyGroupsOptions: { shortenMillia: true } }))
.toBe(shortMillia2);

const exp = '1e+3003003';
const shortMillia3 = 'one millia^2-unmilliatillion';
expect(stringify(exp, { stringifyGroupsOptions: { shortenMillia: true } })).toBe(shortMillia3);
expect(parse(shortMillia3)).toBe('1e+3003003');
});
});

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

@@ -0,0 +1,335 @@
import { describe, it, expect } from 'vitest';
import {
AllowedValue,
parse,
stringify,
systems,
} from '../../../src';

const options = {
system: systems.enUS.shortCount,
};

const doExpect = (
value: AllowedValue,
stringified: string,
parsedValue: AllowedValue = value,
) => {
const stringifyOptions = {
...options,
};

const parseOptions = {
...options,
type: typeof value as ('number' | 'string' | 'bigint'),
};

expect(stringify(value, stringifyOptions)).toBe(stringified);
expect(parse(stringified, parseOptions)).toBe(parsedValue);
};

describe('American short count', () => {
describe('individual cases', () => {
const longNumberValue1 = '123,456,789,012,345,678,901,234,567,890';
const longNumberStringified1 = [
'one hundred twenty-three octillion',
'four hundred fifty-six septillion',
'seven hundred eighty-nine sextillion',
'twelve quintillion',
'three hundred forty-five quadrillion',
'six hundred seventy-eight trillion',
'nine hundred one billion',
'two hundred thirty-four million',
'five hundred sixty-seven thousand',
'eight hundred ninety',
].join(' ');

const longNumberParsedValue = '1.2345678901234567890123456789e+29';

it.each`
value | stringified | parsedValue
${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'}
${longNumberValue1} | ${longNumberStringified1} | ${longNumberParsedValue}
${'1e3006'} | ${'one milliauntillion'} | ${'1e+3006'}
`('converts $value to $stringified', ({
value,
stringified,
parsedValue,
}: { value: AllowedValue, stringified: string, parsedValue: AllowedValue }) => {
doExpect(value, stringified, parsedValue);
});

it('converts one millia^1-tillion to 1e+3003', () => {
expect(parse('one millia^1-tillion')).toBe('1e+3003');
});
});

describe('blanket cases', () => {
it('converts one millia^2-tillion', () => {
const value = '1e+3000003';
const stringified = 'one millia^2-tillion';
expect(parse(stringified)).toBe(value);
expect(stringify(value, { stringifyGroupsOptions: { shortenMillia: true } }))
.toBe(stringified);
});

it('converts one millia^2-unmilliatillion', () => {
const value = '1e+3003003';
const stringified = 'one millia^2-unmilliatillion';
expect(stringify(value, { stringifyGroupsOptions: { shortenMillia: true } }))
.toBe(stringified);
expect(parse(stringified)).toBe(value);
});

it('converts one milliamilliaunmilliatillion', () => {
const value = '1e+3003003';
const stringified = 'one milliamilliaunmilliatillion';
expect(stringify(value)).toBe(stringified);
expect(parse(stringified)).toBe('1e+3003003');
});
});

describe('general', () => {
describe('0-9', () => {
it.each`
value | numberName
${0} | ${'zero'}
${1} | ${'one'}
${2} | ${'two'}
${3} | ${'three'}
${4} | ${'four'}
${5} | ${'five'}
${6} | ${'six'}
${7} | ${'seven'}
${8} | ${'eight'}
${9} | ${'nine'}
`('converts $ones to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe('10-19', () => {
it.each`
value | numberName
${10} | ${'ten'}
${11} | ${'eleven'}
${12} | ${'twelve'}
${13} | ${'thirteen'}
${14} | ${'fourteen'}
${15} | ${'fifteen'}
${16} | ${'sixteen'}
${17} | ${'seventeen'}
${18} | ${'eighteen'}
${19} | ${'nineteen'}
`('converts $tenPlusOnes to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
tensStart | tensEnd | tensBase
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$tensStart-$tensEnd', ({
tensStart, tensBase,
}: { tensStart: number, tensBase: string }) => {
it.each`
value | numberName
${tensStart} | ${tensBase}
${tensStart + 1} | ${`${tensBase}-one`}
${tensStart + 2} | ${`${tensBase}-two`}
${tensStart + 3} | ${`${tensBase}-three`}
${tensStart + 4} | ${`${tensBase}-four`}
${tensStart + 5} | ${`${tensBase}-five`}
${tensStart + 6} | ${`${tensBase}-six`}
${tensStart + 7} | ${`${tensBase}-seven`}
${tensStart + 8} | ${`${tensBase}-eight`}
${tensStart + 9} | ${`${tensBase}-nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
hundredsStart | hundredsEnd | hundredsBase
${100} | ${199} | ${'one hundred'}
${200} | ${299} | ${'two hundred'}
${300} | ${399} | ${'three hundred'}
${400} | ${499} | ${'four hundred'}
${500} | ${599} | ${'five hundred'}
${600} | ${699} | ${'six hundred'}
${700} | ${799} | ${'seven hundred'}
${800} | ${899} | ${'eight hundred'}
${900} | ${999} | ${'nine hundred'}
`('$hundredsStart-$hundredsEnd', ({
hundredsStart, hundredsBase,
}: { hundredsStart: number, hundredsBase: string }) => {
describe(`${hundredsStart}-${hundredsStart + 9}`, () => {
it.each`
value | numberName
${hundredsStart} | ${hundredsBase}
${hundredsStart + 1} | ${`${hundredsBase} one`}
${hundredsStart + 2} | ${`${hundredsBase} two`}
${hundredsStart + 3} | ${`${hundredsBase} three`}
${hundredsStart + 4} | ${`${hundredsBase} four`}
${hundredsStart + 5} | ${`${hundredsBase} five`}
${hundredsStart + 6} | ${`${hundredsBase} six`}
${hundredsStart + 7} | ${`${hundredsBase} seven`}
${hundredsStart + 8} | ${`${hundredsBase} eight`}
${hundredsStart + 9} | ${`${hundredsBase} nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe(`${hundredsStart + 10}-${hundredsStart + 19}`, () => {
it.each`
value | numberName
${hundredsStart + 10} | ${`${hundredsBase} ten`}
${hundredsStart + 11} | ${`${hundredsBase} eleven`}
${hundredsStart + 12} | ${`${hundredsBase} twelve`}
${hundredsStart + 13} | ${`${hundredsBase} thirteen`}
${hundredsStart + 14} | ${`${hundredsBase} fourteen`}
${hundredsStart + 15} | ${`${hundredsBase} fifteen`}
${hundredsStart + 16} | ${`${hundredsBase} sixteen`}
${hundredsStart + 17} | ${`${hundredsBase} seventeen`}
${hundredsStart + 18} | ${`${hundredsBase} eighteen`}
${hundredsStart + 19} | ${`${hundredsBase} nineteen`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});

describe.each`
start | end | base
${20} | ${29} | ${'twenty'}
${30} | ${39} | ${'thirty'}
${40} | ${49} | ${'forty'}
${50} | ${59} | ${'fifty'}
${60} | ${69} | ${'sixty'}
${70} | ${79} | ${'seventy'}
${80} | ${89} | ${'eighty'}
${90} | ${99} | ${'ninety'}
`('$start-$end', ({
start, base,
}: { start: number, base: string }) => {
it.each`
value | numberName
${hundredsStart + start} | ${`${hundredsBase} ${base}`}
${hundredsStart + start + 1} | ${`${hundredsBase} ${base}-one`}
${hundredsStart + start + 2} | ${`${hundredsBase} ${base}-two`}
${hundredsStart + start + 3} | ${`${hundredsBase} ${base}-three`}
${hundredsStart + start + 4} | ${`${hundredsBase} ${base}-four`}
${hundredsStart + start + 5} | ${`${hundredsBase} ${base}-five`}
${hundredsStart + start + 6} | ${`${hundredsBase} ${base}-six`}
${hundredsStart + start + 7} | ${`${hundredsBase} ${base}-seven`}
${hundredsStart + start + 8} | ${`${hundredsBase} ${base}-eight`}
${hundredsStart + start + 9} | ${`${hundredsBase} ${base}-nine`}
`('converts $value to $numberName', ({ value, numberName }: { value: number, numberName: string }) => {
doExpect(value, numberName);
});
});
});
});

it.each`
value | numberName
${'1e+6'} | ${'one million'}
${'1e+9'} | ${'one billion'}
${'1e+12'} | ${'one trillion'}
${'1e+15'} | ${'one quadrillion'}
${'1e+18'} | ${'one quintillion'}
${'1e+21'} | ${'one sextillion'}
${'1e+24'} | ${'one septillion'}
${'1e+27'} | ${'one octillion'}
${'1e+30'} | ${'one nonillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+33'} | ${'one decillion'}
${'1e+36'} | ${'one undecillion'}
${'1e+39'} | ${'one duodecillion'}
${'1e+42'} | ${'one tredecillion'}
${'1e+45'} | ${'one quattuordecillion'}
${'1e+48'} | ${'one quindecillion'}
${'1e+51'} | ${'one sexdecillion'}
${'1e+54'} | ${'one septendecillion'}
${'1e+57'} | ${'one octodecillion'}
${'1e+60'} | ${'one novemdecillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+63'} | ${'one vigintillion'}
${'1e+66'} | ${'one unvigintillion'}
${'1e+69'} | ${'one duovigintillion'}
${'1e+72'} | ${'one trevigintillion'}
${'1e+75'} | ${'one quattuorvigintillion'}
${'1e+78'} | ${'one quinvigintillion'}
${'1e+81'} | ${'one sexvigintillion'}
${'1e+84'} | ${'one septenvigintillion'}
${'1e+87'} | ${'one octovigintillion'}
${'1e+90'} | ${'one novemvigintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+93'} | ${'one trigintillion'}
${'1e+123'} | ${'one quadragintillion'}
${'1e+153'} | ${'one quinquagintillion'}
${'1e+183'} | ${'one sexagintillion'}
${'1e+213'} | ${'one septuagintillion'}
${'1e+243'} | ${'one octogintillion'}
${'1e+273'} | ${'one nonagintillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it.each`
value | numberName
${'1e+303'} | ${'one centillion'}
${'1e+306'} | ${'one cenuntillion'}
${'1e+309'} | ${'one cenduotillion'}
${'1e+312'} | ${'one centretillion'}
${'1e+315'} | ${'one cenquattuortillion'}
${'1e+318'} | ${'one cenquintillion'}
${'1e+321'} | ${'one censextillion'}
${'1e+324'} | ${'one censeptentillion'}
${'1e+327'} | ${'one cenoctotillion'}
${'1e+330'} | ${'one cennovemtillion'}
${'1e+603'} | ${'one duocentillion'}
${'1e+903'} | ${'one trecentillion'}
${'1e+1203'} | ${'one quadringentillion'}
${'1e+1503'} | ${'one quingentillion'}
${'1e+1803'} | ${'one sescentillion'}
${'1e+2103'} | ${'one septingentillion'}
${'1e+2403'} | ${'one octingentillion'}
${'1e+2703'} | ${'one nongentillion'}
`('converts $value to $numberName', ({ value, numberName }: { value: string, numberName: string }) => {
doExpect(value, numberName);
});

it('converts \'1e+3003\' to \'one milliatillion\'', () => {
doExpect('1e+3003', 'one milliatillion');
});
});

Loading…
Cancel
Save