|
- 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<TStringifyGroupsOptions, TMergeTokensOptions>,
- ): 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;
- };
|