import { enUS } from './systems'; import { NumberNameSystem } from './common'; import { exponentialToNumberString, extractExponentialComponents, numberToExponential } from './exponent'; /** * Negative symbol. */ const NEGATIVE_SYMBOL = '-' as const; /** * Exponent delimiter. */ const EXPONENT_DELIMITER = 'e' as const; /** * Allowed value type for {@link stringify}. */ 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. */ export interface StringifyOptions< 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?: NumberNameSystem; /** * Options to use when stringifying a single group. */ stringifyGroupsOptions?: TStringifyGroupsOptions; /** * Options to use when merging tokens. */ mergeTokensOptions?: TMergeTokensOptions; } /** * Converts a numeric value to its name. * @param value - The value to convert. * @param options - Options to use when converting a value to its name. * @returns string The name of the value. */ 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)) )) { throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof value}`); } const valueStr = value.toString().replace(/\s/g, ''); 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.splitIntoGroups(valueStr); const groupNames = system.stringifyGroups(groups, stringifyGroupsOptions); return system.mergeTokens(groupNames, mergeTokensOptions); }; /** * Options to use when parsing a name of a number to its numeric equivalent. */ export interface ParseOptions { /** * The system to use when parsing a name of a number to its numeric equivalent. * * Defaults to en-US (American short count). */ system?: NumberNameSystem; /** * The type to parse the value as. */ type?: ParseResult; } /** * Parses a name of a number to its numeric equivalent. * @param value - The value to parse. * @param options - Options to use when parsing a name of a number to its numeric equivalent. * @returns AllowedValue The numeric equivalent of the value. */ export const parse = (value: string, options = {} as ParseOptions) => { const { system = enUS.shortCount, type = 'string' } = options; const tokens = system.tokenize(value); const groups = system.parseGroups(tokens); const stringValue = system.combineGroups(groups); switch (type) { case 'number': { // Precision might be lost here. Use bigint when not using fractional parts. if (stringValue.includes(EXPONENT_DELIMITER)) { const { exponent, integer } = extractExponentialComponents(stringValue); const exponentValue = Number(exponent); const integerValue = Number(integer); const [maxSafeIntegerSignificand, maxSafeIntegerExponent] = Number.MAX_SAFE_INTEGER.toExponential().split('e'); if ( exponentValue >= Number(maxSafeIntegerExponent) && integerValue >= Math.floor(Number(maxSafeIntegerSignificand)) ) { // greater than Number.MAX_SAFE_INTEGER const logger = console; logger.warn(`Value too large to be produced as number: ${value}`); logger.warn('Falling back to string...'); return stringValue; } const [epsilonSignificand, epsilonExponent] = Number.EPSILON.toExponential().split('e'); if ( exponentValue <= Number(epsilonExponent) && integerValue <= Math.floor(Number(epsilonSignificand)) ) { // smaller than Number.EPSILON const logger = console; logger.warn(`Value too small to be produced as number: ${value}`); logger.warn('Falling back to string...'); return stringValue; } } return Number(stringValue); } case 'bigint': { const normalizedNumberString = exponentialToNumberString(numberToExponential(stringValue)); try { return BigInt(normalizedNumberString); } catch { const logger = console; logger.warn(`Value too long to be produced as bigint: ${value}`); logger.warn('Falling back to string...'); } return stringValue; } default: break; } return stringValue; };