Browse Source

Update documentation, types

Add features and limitations to the project.

Also update the types to be stricter.
master
TheoryOfNekomata 8 months ago
parent
commit
fa9c981c07
9 changed files with 109 additions and 64 deletions
  1. +19
    -0
      README.md
  2. +2
    -2
      TODO.md
  3. +33
    -2
      packages/core/src/common.ts
  4. +8
    -22
      packages/core/src/converter.ts
  5. +9
    -6
      packages/core/src/exponent.ts
  6. +13
    -10
      packages/core/src/systems/en-UK/long-count/parse.ts
  7. +6
    -6
      packages/core/src/systems/en-UK/long-count/stringify.ts
  8. +13
    -10
      packages/core/src/systems/en-US/short-count/parse.ts
  9. +6
    -6
      packages/core/src/systems/en-US/short-count/stringify.ts

+ 19
- 0
README.md View File

@@ -5,3 +5,22 @@ Get the name of a number, even if it's stupidly big.
## References

* [How high can you count?](http://www.isthe.com/chongo/tech/math/number/howhigh.html)

## Features

* Stringify/parse numbers and names in American/British short count (e.g. "million", "billion", "trillion"), or European
(e.g. "million", "milliard", "billion", "billiard", "trillion", "trilliard").
* Support for exponential notation, even if in non-standard form (e.g. `123.45e+5`).
* Support for negative numbers.

See [TODO.md](TODO.md) for a list of features that are planned for implementation.

## Limitations

* Can only stringify and parse numbers and names that resolve to integer values.
* Exponents `x` in values are limited to `Number.MAX_SAFE_INTEGER >~ x >~ Number.MIN_SAFE_INTEGER`.
Values may exceed extremes such as `Number.MAX_SAFE_INTEGER` and `Number.EPSILON` in which loss of
precision may occur.
* No support for arbitrary number names such as "googol".
* No support for fractional number names such as "half", "quarter", "third", "tenth", etc.
* Supports only native types (`bigint`, `string`, `number`).

+ 2
- 2
TODO.md View File

@@ -1,3 +1,3 @@
- [ ] Ordinals
- [ ] Fractions
- [ ] Other locales (long count, languages, etc.)
- [ ] ~~Fractions~~
- [X] Other locales (long count, languages, etc.)

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

@@ -1,7 +1,9 @@
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

/**
* Group digits.
*/
type GroupDigits = string;
export type GroupDigits = `${Digit}${Digit}${Digit}`;

/**
* Group place.
@@ -102,7 +104,7 @@ export interface NumberNameSystem {
* Parses groups from a string.
* @param tokens - The string to parse groups from.
* @see {NumberNameSystem.stringifyGroups}
* @returns Group[] The parsed groups.
* @returns ParseResult The parsed groups as well as an indicator if the number is Boolean.
*/
parseGroups: (tokens: string[]) => ParseResult;
/**
@@ -115,6 +117,25 @@ export interface NumberNameSystem {
combineGroups: (groups: Group[], negative: boolean) => string;
}

/**
* Allowed value type for {@link stringify}.
*/
export type AllowedValue = string | number | bigint;

/**
* Array of allowed types for {@link parse}.
*/
export const ALLOWED_PARSE_RESULT_TYPES = [
'string',
'number',
'bigint',
] as const;

/**
* Allowed type for {@link parse}.
*/
export type ParseResultType = typeof ALLOWED_PARSE_RESULT_TYPES[number];

/**
* Error thrown when an invalid token is encountered.
*/
@@ -124,6 +145,11 @@ export class InvalidTokenError extends Error {
}
}

/**
* Gets the maximum value from a list of bigints.
* @param b - The bigints to get the maximum value from.
* @returns bigint The maximum value.
*/
export const bigIntMax = (...b: bigint[]) => b.reduce(
(previousMax, current) => {
if (typeof previousMax === 'undefined') {
@@ -134,6 +160,11 @@ export const bigIntMax = (...b: bigint[]) => b.reduce(
undefined as bigint | undefined,
);

/**
* Gets the minimum value from a list of bigints.
* @param b - The bigints to get the minimum value from.
* @returns bigint The minimum value.
*/
export const bigIntMin = (...b: bigint[]) => b.reduce(
(previousMin, current) => {
if (typeof previousMin === 'undefined') {


+ 8
- 22
packages/core/src/converter.ts View File

@@ -1,5 +1,10 @@
import { enUS } from './systems';
import { NumberNameSystem } from './common';
import {
ALLOWED_PARSE_RESULT_TYPES,
AllowedValue,
NumberNameSystem,
ParseResultType,
} from './common';
import { exponentialToNumberString, extractExponentialComponents, numberToExponential } from './exponent';

/**
@@ -12,25 +17,6 @@ const NEGATIVE_SYMBOL = '-' as const;
*/
const EXPONENT_DELIMITER = 'e' as const;

/**
* Allowed value type for {@link stringify}.
*/
export type AllowedValue = string | number | bigint;

/**
* Array of allowed types for {@link parse}.
*/
const ALLOWED_PARSE_RESULT_TYPES = [
'string',
'number',
'bigint',
] as const;

/**
* Allowed type for {@link parse}.
*/
type ParseResult = typeof ALLOWED_PARSE_RESULT_TYPES[number];

/**
* Options to use when converting a value to a string.
*/
@@ -104,7 +90,7 @@ export interface ParseOptions {
/**
* The type to parse the value as.
*/
type?: ParseResult;
type?: ParseResultType;
}

/**
@@ -115,7 +101,7 @@ export interface ParseOptions {
*/
export const parse = (value: string, options = {} as ParseOptions) => {
const { system = enUS.shortCount, type: typeRaw = 'string' } = options;
const type = typeRaw.trim().toLowerCase() as ParseResult;
const type = typeRaw.trim().toLowerCase() as ParseResultType;

if (!((ALLOWED_PARSE_RESULT_TYPES as unknown as string[]).includes(type))) {
throw new TypeError(`Return type must be a string, number, or bigint. Received: ${type}`);


+ 9
- 6
packages/core/src/exponent.ts View File

@@ -1,7 +1,4 @@
/**
* Valid values that can be converted to exponential notation.
*/
export type ValidValue = string | number | bigint;
import { AllowedValue } from './common';

interface BaseOptions {
/**
@@ -158,13 +155,13 @@ export const extractExponentialComponents = (
};

/**
* Converts a numeric value to a string in exponential notation. Supports numbers of all types.
* Converts a numeric value to a string in exponential notation.
* @param value - The value to convert.
* @param options - Options to use when extracting components.
* @returns string The value in exponential notation.
*/
export const numberToExponential = (
value: ValidValue,
value: AllowedValue,
options = {} as NumberToExponentialOptions,
): string => {
const valueRaw = value as unknown;
@@ -216,6 +213,12 @@ export const numberToExponential = (
return `${significandInteger}${decimalPoint}${significandFractional}${exponentDelimiter}${exponent}`;
};

/**
* Converts a string in exponential notation (e.g. 1e+2) to a number string (e.g. 100).
* @param exp - The number in exponential notation to convert.
* @param options - Options to use when converting number strings.
* @returns string The number string.
*/
export const exponentialToNumberString = (
exp: string,
options = {} as ExponentialToNumberStringOptions,


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

@@ -2,7 +2,7 @@ import {
bigIntMax, bigIntMin,
Group,
GROUP_DIGITS_INDEX,
GROUP_PLACE_INDEX,
GROUP_PLACE_INDEX, GroupDigits,
InvalidTokenError,
} from '../../../common';
import {
@@ -269,14 +269,14 @@ const parseThousand = (acc: ParserState, token: string): ParserState => {
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}`;
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
}
} 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)}`
);
) as GroupDigits;
}
}

@@ -294,7 +294,7 @@ const parseThousand = (acc: ParserState, token: string): ParserState => {
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)}`;
lastGroup[GROUP_DIGITS_INDEX] = `${hundreds}${lastGroup[GROUP_DIGITS_INDEX].slice(1)}` as GroupDigits;
return {
...acc,
groups: [...acc.groups.slice(0, -1), lastGroup],
@@ -308,7 +308,7 @@ const parseFinal = (acc: ParserState): ParserState => {
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}`;
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
}
// We assume last token without parsed place will always be the smallest
lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
@@ -322,7 +322,9 @@ const parseFinal = (acc: ParserState): ParserState => {
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_DIGITS_INDEX] = (
`${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
) as GroupDigits;
}
lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
return {
@@ -360,11 +362,11 @@ const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => {
...acc,
lastToken: token,
mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
groups: [...acc.groups, [`01${tenPlusOnes}` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
};
}

lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}`;
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}` as GroupDigits;
return {
...acc,
lastToken: token,
@@ -381,13 +383,14 @@ const parseTens = (acc: ParserState, token: string): ParserState => {
...acc,
lastToken: token,
mode: ParseGroupsMode.TENS_MODE,
groups: [...acc.groups, [`0${tens}0`, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
groups: [...acc.groups, [`0${tens}0` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
};
}

lastGroup[GROUP_DIGITS_INDEX] = (
`${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
);
) as GroupDigits;

return {
...acc,
lastToken: token,


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

@@ -1,7 +1,7 @@
import {
Group,
GROUP_DIGITS_INDEX,
GROUP_PLACE_INDEX,
GROUP_PLACE_INDEX, GroupDigits,
GroupPlace,
} from '../../../common';
import { numberToExponential } from '../../../exponent';
@@ -179,16 +179,16 @@ const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
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;
newGroup[GROUP_DIGITS_INDEX] = c as GroupDigits;
return [newGroup];
}

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

newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
newGroup[GROUP_DIGITS_INDEX] = (c + firstGroup[0]) as GroupDigits;
return [
newGroup,
...acc.slice(1),
@@ -309,11 +309,11 @@ export const splitIntoGroups = (value: string): Group[] => {
const lastGroupDigits = lastGroup[0].split('');
lastGroupDigits[currentPlaceInGroup] = c;
return [...acc.slice(0, -1) ?? [], [
lastGroupDigits.join(''),
lastGroupDigits.join('') as GroupDigits,
currentPlace,
]];
}
return [...acc, [c.padEnd(3, '0'), currentPlace]];
return [...acc, [c.padEnd(3, '0') as GroupDigits, currentPlace]];
},
[],
);


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

@@ -2,7 +2,7 @@ import {
bigIntMax, bigIntMin,
Group,
GROUP_DIGITS_INDEX,
GROUP_PLACE_INDEX,
GROUP_PLACE_INDEX, GroupDigits,
InvalidTokenError,
} from '../../../common';
import {
@@ -267,14 +267,14 @@ const parseThousand = (acc: ParserState, token: string): ParserState => {
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}`;
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
}
} 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)}`
);
) as GroupDigits;
}
}

@@ -292,7 +292,7 @@ const parseThousand = (acc: ParserState, token: string): ParserState => {
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)}`;
lastGroup[GROUP_DIGITS_INDEX] = `${hundreds}${lastGroup[GROUP_DIGITS_INDEX].slice(1)}` as GroupDigits;
return {
...acc,
groups: [...acc.groups.slice(0, -1), lastGroup],
@@ -306,7 +306,7 @@ const parseFinal = (acc: ParserState): ParserState => {
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}`;
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}` as GroupDigits;
}
// We assume last token without parsed place will always be the smallest
lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
@@ -320,7 +320,9 @@ const parseFinal = (acc: ParserState): ParserState => {
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_DIGITS_INDEX] = (
`${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
) as GroupDigits;
}
lastGroup[GROUP_PLACE_INDEX] = BigInt(0);
return {
@@ -358,11 +360,11 @@ const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => {
...acc,
lastToken: token,
mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
groups: [...acc.groups, [`01${tenPlusOnes}` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
};
}

lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}`;
lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}` as GroupDigits;
return {
...acc,
lastToken: token,
@@ -379,13 +381,14 @@ const parseTens = (acc: ParserState, token: string): ParserState => {
...acc,
lastToken: token,
mode: ParseGroupsMode.TENS_MODE,
groups: [...acc.groups, [`0${tens}0`, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
groups: [...acc.groups, [`0${tens}0` as GroupDigits, lastGroup[GROUP_PLACE_INDEX] - BigInt(1)]],
};
}

lastGroup[GROUP_DIGITS_INDEX] = (
`${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
);
) as GroupDigits;

return {
...acc,
lastToken: token,


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

@@ -1,7 +1,7 @@
import {
Group,
GROUP_DIGITS_INDEX,
GROUP_PLACE_INDEX,
GROUP_PLACE_INDEX, GroupDigits,
GroupPlace,
} from '../../../common';
import { numberToExponential } from '../../../exponent';
@@ -177,16 +177,16 @@ const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
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;
newGroup[GROUP_DIGITS_INDEX] = c as GroupDigits;
return [newGroup];
}

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

newGroup[GROUP_DIGITS_INDEX] = c + firstGroup[0];
newGroup[GROUP_DIGITS_INDEX] = (c + firstGroup[0]) as GroupDigits;
return [
newGroup,
...acc.slice(1),
@@ -307,11 +307,11 @@ export const splitIntoGroups = (value: string): Group[] => {
const lastGroupDigits = lastGroup[0].split('');
lastGroupDigits[currentPlaceInGroup] = c;
return [...acc.slice(0, -1), [
lastGroupDigits.join(''),
lastGroupDigits.join('') as GroupDigits,
currentPlace,
]];
}
return [...acc, [c.padEnd(3, '0'), currentPlace]];
return [...acc, [c.padEnd(3, '0') as GroupDigits, currentPlace]];
},
[],
);


Loading…
Cancel
Save