diff --git a/packages/core/src/common.ts b/packages/core/src/common.ts index c3e19aa..b69b2b1 100644 --- a/packages/core/src/common.ts +++ b/packages/core/src/common.ts @@ -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: (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: (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: (tokens: string[], options?: T) => string; + mergeTokens: (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; } /** diff --git a/packages/core/src/converter.ts b/packages/core/src/converter.ts index 6456c66..37dde11 100644 --- a/packages/core/src/converter.ts +++ b/packages/core/src/converter.ts @@ -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, + ): 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. */ diff --git a/packages/core/src/systems/en-US/common.ts b/packages/core/src/systems/en-US/common.ts index 2c7a12e..4c3ef48 100644 --- a/packages/core/src/systems/en-US/common.ts +++ b/packages/core/src/systems/en-US/common.ts @@ -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. */ diff --git a/packages/core/src/systems/en-US/short-count/parse.ts b/packages/core/src/systems/en-US/short-count/parse.ts index d290aab..fa6a40a 100644 --- a/packages/core/src/systems/en-US/short-count/parse.ts +++ b/packages/core/src/systems/en-US/short-count/parse.ts @@ -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) { diff --git a/packages/core/src/systems/en-US/short-count/stringify.ts b/packages/core/src/systems/en-US/short-count/stringify.ts index e4cc2d6..2514838 100644 --- a/packages/core/src/systems/en-US/short-count/stringify.ts +++ b/packages/core/src/systems/en-US/short-count/stringify.ts @@ -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) diff --git a/packages/core/test/systems/en-US.test.ts b/packages/core/test/systems/en-US.test.ts index 44e2ae7..2da2c7e 100644 --- a/packages/core/test/systems/en-US.test.ts +++ b/packages/core/test/systems/en-US.test.ts @@ -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); + }); }); diff --git a/packages/core/test/systems/en-US/chongo.test.ts b/packages/core/test/systems/en-US/chongo.test.ts index 0d7e505..3b67fa7 100644 --- a/packages/core/test/systems/en-US/chongo.test.ts +++ b/packages/core/test/systems/en-US/chongo.test.ts @@ -3,7 +3,7 @@ import { stringify, parse } from '../../../src'; import { numberToExponential } from '../../../src/exponent'; const stringifyOptions = { - makeGroupOptions: { + stringifyGroupsOptions: { addTensDashes: false, }, };