Browse Source

Fix short millia

Make short millia parse and stringify correctly.
master
TheoryOfNekomata 1 year ago
parent
commit
d227f3e09f
7 changed files with 196 additions and 79 deletions
  1. +27
    -15
      packages/core/src/common.ts
  2. +23
    -19
      packages/core/src/converter.ts
  3. +4
    -0
      packages/core/src/systems/en-US/common.ts
  4. +93
    -33
      packages/core/src/systems/en-US/short-count/parse.ts
  5. +37
    -10
      packages/core/src/systems/en-US/short-count/stringify.ts
  6. +11
    -1
      packages/core/test/systems/en-US.test.ts
  7. +1
    -1
      packages/core/test/systems/en-US/chongo.test.ts

+ 27
- 15
packages/core/src/common.ts View File

@@ -48,44 +48,56 @@ export const GROUP_PLACE_INDEX = 1 as const;
/** /**
* System for stringifying and parsing numbers. * System for stringifying and parsing numbers.
*/ */
export interface StringifySystem {
export interface NumberNameSystem {
/** /**
* Creates a negative string. * Creates a negative string.
* @param s - The string to make negative. * @param s - The string to make negative.
* @returns string The negative string.
*/ */
makeNegative: (s: string) => string; makeNegative: (s: string) => string;
/** /**
* Creates a group string.
* @param group - The group digits.
* @param place - The group place.
* @param options - Options to use when creating the group.
* Splits a string into groups.
* @param value - The string to group.
* @see {NumberNameSystem.combineGroups}
* @returns Group[] The groups.
*/ */
makeGroups: <T extends object>(groups: Group[], options?: T) => string[];
splitIntoGroups: (value: string) => Group[];
/** /**
* Groups a string.
* @param value - The string to group.
* Creates a group string.
* @param groups - The groups.
* @param options - Options to use when creating the group.
* @see {NumberNameSystem.parseGroups}
* @returns string[] The groups represented into strings.
*/ */
group: (value: string) => Group[];
stringifyGroups: <T extends object>(groups: Group[], options?: T) => string[];
/** /**
* Finalizes a string.
* Merges tokens from stringified groups to a string.
* @param tokens - The tokens to finalize. * @param tokens - The tokens to finalize.
* @see {NumberNameSystem.tokenize}
* @returns string The merged tokens.
*/ */
finalize: <T extends object>(tokens: string[], options?: T) => string;
mergeTokens: <T extends object>(tokens: string[], options?: T) => string;
/** /**
* Tokenizes a string. * Tokenizes a string.
* @param value - The string to tokenize. * @param value - The string to tokenize.
* @see {NumberNameSystem.mergeTokens}
* @returns string[] The tokens.
*/ */
tokenize: (value: string) => string[]; tokenize: (value: string) => string[];
/** /**
* Parses groups from a string. * Parses groups from a string.
* @param value - The string to parse groups from.
* @param tokens - The string to parse groups from.
* @see {NumberNameSystem.stringifyGroups}
* @returns Group[] The parsed groups.
*/ */
parseGroups: (value: string[]) => Group[];
parseGroups: (tokens: string[]) => Group[];
/** /**
* Combines groups into a string. * Combines groups into a string.
* @param value - The groups to combine.
* @param groups - The groups to combine.
* @see {NumberNameSystem.splitIntoGroups}
* @returns string The combined groups in exponential form.
*/ */
combineGroups: (value: Group[]) => string;
combineGroups: (groups: Group[]) => string;
} }


/** /**


+ 23
- 19
packages/core/src/converter.ts View File

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


/** /**
@@ -35,20 +35,23 @@ type ParseResult = typeof ALLOWED_PARSE_RESULT_TYPES[number];
* Options to use when converting a value to a string. * Options to use when converting a value to a string.
*/ */
export interface StringifyOptions< export interface StringifyOptions<
TMakeGroupOptions extends object = object,
TFinalizeOptions extends object = object,
TStringifyGroupsOptions extends object,
TMergeTokensOptions extends object,
> { > {
/** /**
* The system to use when converting a value to a string. * The system to use when converting a value to a string.
* *
* Defaults to en-US (American short count). * Defaults to en-US (American short count).
*/ */
system?: StringifySystem;
system?: NumberNameSystem;
/** /**
* Options to use when making a group. This is used to override the default options for a group.
* Options to use when stringifying a single group.
*/ */
makeGroupOptions?: TMakeGroupOptions;
finalizeOptions?: TFinalizeOptions;
stringifyGroupsOptions?: TStringifyGroupsOptions;
/**
* Options to use when merging tokens.
*/
mergeTokensOptions?: TMergeTokensOptions;
} }


/** /**
@@ -57,10 +60,14 @@ export interface StringifyOptions<
* @param options - Options to use when converting a value to its name. * @param options - Options to use when converting a value to its name.
* @returns string The name of the value. * @returns string The name of the value.
*/ */
export const stringify = (
value: AllowedValue,
options = {} as StringifyOptions,
): string => {
export const stringify = <
TStringifyGroupsOptions extends object,
TMergeTokensOptions extends object
>
(
value: AllowedValue,
options = {} as StringifyOptions<TStringifyGroupsOptions, TMergeTokensOptions>,
): string => {
if (!( if (!(
(ALLOWED_PARSE_RESULT_TYPES as unknown as string[]) (ALLOWED_PARSE_RESULT_TYPES as unknown as string[])
.includes(typeof (value as unknown)) .includes(typeof (value as unknown))
@@ -69,18 +76,15 @@ export const stringify = (
} }


const valueStr = value.toString().replace(/\s/g, ''); const valueStr = value.toString().replace(/\s/g, '');
const { system = enUS.shortCount, makeGroupOptions, finalizeOptions } = options;
const { system = enUS.shortCount, stringifyGroupsOptions, mergeTokensOptions } = options;


if (valueStr.startsWith(NEGATIVE_SYMBOL)) { if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options)); return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
} }


const groups = system.group(valueStr);
const groupNames = system.makeGroups(
groups,
makeGroupOptions,
);
return system.finalize(groupNames, finalizeOptions);
const groups = system.splitIntoGroups(valueStr);
const groupNames = system.stringifyGroups(groups, stringifyGroupsOptions);
return system.mergeTokens(groupNames, mergeTokensOptions);
}; };


/** /**
@@ -92,7 +96,7 @@ export interface ParseOptions {
* *
* Defaults to en-US (American short count). * Defaults to en-US (American short count).
*/ */
system?: StringifySystem;
system?: NumberNameSystem;
/** /**
* The type to parse the value as. * The type to parse the value as.
*/ */


+ 4
- 0
packages/core/src/systems/en-US/common.ts View File

@@ -16,12 +16,16 @@ export const POSITIVE_SYMBOL = '+' as const;


export const SHORT_MILLIA_DELIMITER = '^' as const; export const SHORT_MILLIA_DELIMITER = '^' as const;


export const SHORT_MILLIA_ILLION_DELIMITER = '-' as const;

export const EXPONENT_DELIMITER = 'e' as const; export const EXPONENT_DELIMITER = 'e' as const;


export const EMPTY_GROUP_DIGITS = '000' as const; export const EMPTY_GROUP_DIGITS = '000' as const;


export const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, BigInt(0)]; export const EMPTY_PLACE: Group = [EMPTY_GROUP_DIGITS, BigInt(0)];


export const T_AFFIX = 't' as const;

/** /**
* Ones number names. * Ones number names.
*/ */


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

@@ -21,6 +21,8 @@ import {
OnesName, OnesName,
POSITIVE_SYMBOL, POSITIVE_SYMBOL,
SHORT_MILLIA_DELIMITER, SHORT_MILLIA_DELIMITER,
SHORT_MILLIA_ILLION_DELIMITER,
T_AFFIX,
TEN_PLUS_ONES, TEN_PLUS_ONES,
TenPlusOnesName, TenPlusOnesName,
TENS, TENS,
@@ -31,12 +33,22 @@ import {


const FINAL_TOKEN = '' as const; const FINAL_TOKEN = '' as const;


export const tokenize = (stringValue: string) => (
stringValue
/**
* Tokenizes a string.
* @param value - The string to tokenize.
* @see {NumberNameSystem.mergeTokens}
* @returns string[] The tokens.
*/
export const tokenize = (value: string) => (
value
.toLowerCase() .toLowerCase()
.trim() .trim()
.replace(/\n+/gs, ' ') .replace(/\n+/gs, ' ')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.replace(
new RegExp(`${SHORT_MILLIA_ILLION_DELIMITER}${T_AFFIX}${ILLION_SUFFIX}`, 'g'),
`${T_AFFIX}${ILLION_SUFFIX}`,
)
.replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ') .replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ')
.split(' ') .split(' ')
.filter((maybeToken) => maybeToken.length > 0) .filter((maybeToken) => maybeToken.length > 0)
@@ -49,19 +61,29 @@ interface DoParseState {
done: boolean; 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 => { const doParseGroupName = (result: DoParseState): DoParseState => {
if (result.groupNameCurrent.length < 1) {
return {
...result,
done: true,
};
}
if (
result.groupNameCurrent.length < 1


if (result.groupNameCurrent === 't') {
// If the current group name is "t", then we're done. // 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. // We use the -t- affix to attach the group prefix to the -illion suffix, except for decillion.
|| result.groupNameCurrent === T_AFFIX
) {
return { return {
...result, ...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, done: true,
}; };
} }
@@ -120,29 +142,23 @@ const doParseGroupName = (result: DoParseState): DoParseState => {
}; };
} }


if (result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`)) {
// short millia
// FIXME
const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/);
if (!matchedMilliaArray) {
throw new InvalidTokenError(result.groupNameCurrent);
}
const matchedMillia = matchedMilliaArray[0];
const newMillia = Number(matchedMillia);
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(MILLIA_PREFIX.length),
done: false,
};
}

if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) { if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) {
const newMillia = result.milliaIndex + 1;
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 oldMillia = result.milliaIndex;
const newMillias = [...result.millias]; const newMillias = [...result.millias];
newMillias[newMillia] = newMillias[oldMillia] || 1; newMillias[newMillia] = newMillias[oldMillia] || 1;
@@ -150,7 +166,7 @@ const doParseGroupName = (result: DoParseState): DoParseState => {
return { return {
milliaIndex: newMillia, milliaIndex: newMillia,
millias: newMillias, millias: newMillias,
groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
groupNameCurrent: result.groupNameCurrent.slice(prefix.length),
done: false, done: false,
}; };
} }
@@ -158,6 +174,11 @@ const doParseGroupName = (result: DoParseState): DoParseState => {
throw new InvalidTokenError(result.groupNameCurrent); 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) => { const getGroupPlaceFromGroupName = (groupName: string) => {
if (groupName === THOUSAND) { if (groupName === THOUSAND) {
return BigInt(1); return BigInt(1);
@@ -191,16 +212,43 @@ const getGroupPlaceFromGroupName = (groupName: string) => {
return bigGroupPlace + BigInt(1); return bigGroupPlace + BigInt(1);
}; };


/**
* Mode of the group parser.
*/
enum ParseGroupsMode { enum ParseGroupsMode {
INITIAL = 'unknown',
/**
* Initial mode.
*/
INITIAL = 'initial',
/**
* Has parsed a ones name.
*/
ONES_MODE = 'ones', ONES_MODE = 'ones',
/**
* Has parsed a tens name.
*/
TENS_MODE = 'tens', TENS_MODE = 'tens',
/**
* Has parsed a ten-plus-ones name.
*/
TEN_PLUS_ONES_MODE = 'tenPlusOnes', TEN_PLUS_ONES_MODE = 'tenPlusOnes',
/**
* Has parsed a "hundred" token.
*/
HUNDRED_MODE = 'hundred', HUNDRED_MODE = 'hundred',
/**
* Has parsed a "thousand" or any "-illion" token.
*/
THOUSAND_MODE = 'thousand', THOUSAND_MODE = 'thousand',
/**
* Done parsing.
*/
DONE = 'done', DONE = 'done',
} }


/**
* State of the group parser.
*/
interface ParserState { interface ParserState {
lastToken?: string; lastToken?: string;
groups: Group[]; groups: Group[];
@@ -340,6 +388,12 @@ const parseTens = (acc: ParserState, token: string): ParserState => {
}; };
}; };


/**
* 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[]) => { export const parseGroups = (tokens: string[]) => {
// We add a final token which is an empty string to parse whatever the last non-empty token is. // We add a final token which is an empty string to parse whatever the last non-empty token is.
const tokensToParse = [...tokens, FINAL_TOKEN]; const tokensToParse = [...tokens, FINAL_TOKEN];
@@ -404,6 +458,12 @@ const bigIntMin = (...b: bigint[]) => b.reduce(
undefined as bigint | undefined, undefined as bigint | undefined,
); );


/**
* 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[]) => { export const combineGroups = (groups: Group[]) => {
const places = groups.map((g) => g[GROUP_PLACE_INDEX]); const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
if (places.length < 1) { if (places.length < 1) {


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

@@ -24,7 +24,7 @@ import {
NEGATIVE, NEGATIVE,
ONES, ONES,
OnesName, OnesName,
SHORT_MILLIA_DELIMITER,
SHORT_MILLIA_DELIMITER, SHORT_MILLIA_ILLION_DELIMITER, T_AFFIX,
TEN_PLUS_ONES, TEN_PLUS_ONES,
TenPlusOnesName, TenPlusOnesName,
TENS, TENS,
@@ -135,6 +135,12 @@ const makeCentillionsPrefix = (
return `${hundredsName}${onesPrefix}${decillions > 0 ? tensName : ''}` as const; 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) => { const repeatString = (s: string, count: GroupPlace) => {
let result = ''; let result = '';
for (let i = BigInt(0); i < count; i += BigInt(1)) { for (let i = BigInt(0); i < count; i += BigInt(1)) {
@@ -183,13 +189,20 @@ const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
.filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS) .filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
.map(([groupDigits, groupPlace], _index, millias) => { .map(([groupDigits, groupPlace], _index, millias) => {
const [hundreds, tens, ones] = groupDigits.split('').map(Number); const [hundreds, tens, ones] = groupDigits.split('').map(Number);
const centillionsPrefix = makeCentillionsPrefix(
hundreds,
tens,
ones,
BigInt(millias.length - 1)
);

if (groupPlace < 1) { if (groupPlace < 1) {
return makeCentillionsPrefix(hundreds, tens, ones, BigInt(millias.length - 1));
return centillionsPrefix;
} }


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


@@ -197,7 +210,7 @@ const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
return milliaSuffix; return milliaSuffix;
} }


return makeCentillionsPrefix(hundreds, tens, ones, BigInt(millias.length - 1)) + milliaSuffix;
return `${centillionsPrefix}${milliaSuffix}`;
}) })
.join(''); .join('');


@@ -206,18 +219,31 @@ const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
} }


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


return `${groupGroups}${ILLION_SUFFIX}` as const; return `${groupGroups}${ILLION_SUFFIX}` as const;
}; };


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


export const makeGroups = (groups: Group[], options?: MakeGroupsOptions): string[] => {
/**
* 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]) => ( const filteredGroups = groups.filter(([digits, place]) => (
place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS
)); ));
@@ -245,8 +271,9 @@ export const makeGroups = (groups: Group[], options?: MakeGroupsOptions): string
/** /**
* Group a number string into groups of three digits, starting from the decimal point. * Group a number string into groups of three digits, starting from the decimal point.
* @param value - The number string to group. * @param value - The number string to group.
* @returns Group[] The groups.
*/ */
export const group = (value: string): Group[] => {
export const splitIntoGroups = (value: string): Group[] => {
const [significand, exponentString] = numberToExponential( const [significand, exponentString] = numberToExponential(
value, value,
{ {
@@ -277,7 +304,7 @@ export const group = (value: string): Group[] => {
); );
}; };


export interface FinalizeOptions {
export interface MergeTokensOptions {
oneGroupPerLine?: boolean; oneGroupPerLine?: boolean;
} }


@@ -286,7 +313,7 @@ export interface FinalizeOptions {
* @param tokens - The tokens to finalize. * @param tokens - The tokens to finalize.
* @param options - The options to use. * @param options - The options to use.
*/ */
export const finalize = (tokens: string[], options?: FinalizeOptions) => (
export const mergeTokens = (tokens: string[], options?: MergeTokensOptions) => (
tokens tokens
.map((t) => t.trim()) .map((t) => t.trim())
.join(options?.oneGroupPerLine ? '\n' : GROUP_SEPARATOR) .join(options?.oneGroupPerLine ? '\n' : GROUP_SEPARATOR)


+ 11
- 1
packages/core/test/systems/en-US.test.ts View File

@@ -8,7 +8,7 @@ const options = {


const stringifyOptions = { const stringifyOptions = {
...options, ...options,
makeGroupOptions: {
stringifyGroupsOptions: {
addTensDashes: false, addTensDashes: false,
}, },
}; };
@@ -286,4 +286,14 @@ describe('numerica', () => {
const value5 = '1e3006'; const value5 = '1e3006';
expect(stringify(value5)).toBe('one milliauntillion'); 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);
});
}); });

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

@@ -3,7 +3,7 @@ import { stringify, parse } from '../../../src';
import { numberToExponential } from '../../../src/exponent'; import { numberToExponential } from '../../../src/exponent';


const stringifyOptions = { const stringifyOptions = {
makeGroupOptions: {
stringifyGroupsOptions: {
addTensDashes: false, addTensDashes: false,
}, },
}; };


Loading…
Cancel
Save