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.
*/
export interface StringifySystem {
export interface NumberNameSystem {
/**
* Creates a negative string.
* @param s - The string to make negative.
* @returns string The negative 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.
* @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.
* @param value - The string to tokenize.
* @see {NumberNameSystem.mergeTokens}
* @returns string[] The tokens.
*/
tokenize: (value: string) => 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.
* @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 { StringifySystem } from './common';
import { NumberNameSystem } from './common';
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.
*/
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.
*
* 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.
* @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 (!(
(ALLOWED_PARSE_RESULT_TYPES as unknown as string[])
.includes(typeof (value as unknown))
@@ -69,18 +76,15 @@ export const stringify = (
}

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)) {
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).
*/
system?: StringifySystem;
system?: NumberNameSystem;
/**
* 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_ILLION_DELIMITER = '-' as const;

export const EXPONENT_DELIMITER = 'e' as const;

export const EMPTY_GROUP_DIGITS = '000' as const;

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

export const T_AFFIX = 't' as const;

/**
* Ones number names.
*/


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

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

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()
.trim()
.replace(/\n+/gs, ' ')
.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'), ' ')
.split(' ')
.filter((maybeToken) => maybeToken.length > 0)
@@ -49,19 +61,29 @@ interface DoParseState {
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) {
return {
...result,
done: true,
};
}
if (
result.groupNameCurrent.length < 1

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

/**
* Mode of the group parser.
*/
enum ParseGroupsMode {
INITIAL = 'unknown',
/**
* 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[];
@@ -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[]) => {
// We add a final token which is an empty string to parse whatever the last non-empty token is.
const tokensToParse = [...tokens, FINAL_TOKEN];
@@ -404,6 +458,12 @@ const bigIntMin = (...b: bigint[]) => b.reduce(
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[]) => {
const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
if (places.length < 1) {


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

@@ -24,7 +24,7 @@ import {
NEGATIVE,
ONES,
OnesName,
SHORT_MILLIA_DELIMITER,
SHORT_MILLIA_DELIMITER, SHORT_MILLIA_ILLION_DELIMITER, T_AFFIX,
TEN_PLUS_ONES,
TenPlusOnesName,
TENS,
@@ -135,6 +135,12 @@ const makeCentillionsPrefix = (
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)) {
@@ -183,13 +189,20 @@ const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
.filter(([groupDigits]) => groupDigits !== EMPTY_GROUP_DIGITS)
.map(([groupDigits, groupPlace], _index, millias) => {
const [hundreds, tens, ones] = groupDigits.split('').map(Number);
const centillionsPrefix = makeCentillionsPrefix(
hundreds,
tens,
ones,
BigInt(millias.length - 1)
);

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

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

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

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

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

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;
};

export interface MakeGroupsOptions {
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;
}

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]) => (
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.
* @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(
value,
{
@@ -277,7 +304,7 @@ export const group = (value: string): Group[] => {
);
};

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

@@ -286,7 +313,7 @@ export interface FinalizeOptions {
* @param tokens - The tokens to finalize.
* @param options - The options to use.
*/
export const finalize = (tokens: string[], options?: FinalizeOptions) => (
export const mergeTokens = (tokens: string[], options?: MergeTokensOptions) => (
tokens
.map((t) => t.trim())
.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 = {
...options,
makeGroupOptions: {
stringifyGroupsOptions: {
addTensDashes: false,
},
};
@@ -286,4 +286,14 @@ describe('numerica', () => {
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);
});
});

+ 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';

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


Loading…
Cancel
Save