Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

169 lines
4.7 KiB

  1. import { enUS } from './systems';
  2. import { NumberNameSystem } from './common';
  3. import { exponentialToNumberString, extractExponentialComponents, numberToExponential } from './exponent';
  4. /**
  5. * Negative symbol.
  6. */
  7. const NEGATIVE_SYMBOL = '-' as const;
  8. /**
  9. * Exponent delimiter.
  10. */
  11. const EXPONENT_DELIMITER = 'e' as const;
  12. /**
  13. * Allowed value type for {@link stringify}.
  14. */
  15. type AllowedValue = string | number | bigint;
  16. /**
  17. * Array of allowed types for {@link parse}.
  18. */
  19. const ALLOWED_PARSE_RESULT_TYPES = [
  20. 'string',
  21. 'number',
  22. 'bigint',
  23. ] as const;
  24. /**
  25. * Allowed type for {@link parse}.
  26. */
  27. type ParseResult = typeof ALLOWED_PARSE_RESULT_TYPES[number];
  28. /**
  29. * Options to use when converting a value to a string.
  30. */
  31. export interface StringifyOptions<
  32. TStringifyGroupsOptions extends object,
  33. TMergeTokensOptions extends object,
  34. > {
  35. /**
  36. * The system to use when converting a value to a string.
  37. *
  38. * Defaults to en-US (American short count).
  39. */
  40. system?: NumberNameSystem;
  41. /**
  42. * Options to use when stringifying a single group.
  43. */
  44. stringifyGroupsOptions?: TStringifyGroupsOptions;
  45. /**
  46. * Options to use when merging tokens.
  47. */
  48. mergeTokensOptions?: TMergeTokensOptions;
  49. }
  50. /**
  51. * Converts a numeric value to its name.
  52. * @param value - The value to convert.
  53. * @param options - Options to use when converting a value to its name.
  54. * @returns string The name of the value.
  55. */
  56. export const stringify = <
  57. TStringifyGroupsOptions extends object,
  58. TMergeTokensOptions extends object
  59. >
  60. (
  61. value: AllowedValue,
  62. options = {} as StringifyOptions<TStringifyGroupsOptions, TMergeTokensOptions>,
  63. ): string => {
  64. if (!(
  65. (ALLOWED_PARSE_RESULT_TYPES as unknown as string[])
  66. .includes(typeof (value as unknown))
  67. )) {
  68. throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof value}`);
  69. }
  70. const valueStr = value.toString().replace(/\s/g, '');
  71. const { system = enUS.shortCount, stringifyGroupsOptions, mergeTokensOptions } = options;
  72. if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
  73. return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
  74. }
  75. const groups = system.splitIntoGroups(valueStr);
  76. const groupNames = system.stringifyGroups(groups, stringifyGroupsOptions);
  77. return system.mergeTokens(groupNames, mergeTokensOptions);
  78. };
  79. /**
  80. * Options to use when parsing a name of a number to its numeric equivalent.
  81. */
  82. export interface ParseOptions {
  83. /**
  84. * The system to use when parsing a name of a number to its numeric equivalent.
  85. *
  86. * Defaults to en-US (American short count).
  87. */
  88. system?: NumberNameSystem;
  89. /**
  90. * The type to parse the value as.
  91. */
  92. type?: ParseResult;
  93. }
  94. /**
  95. * Parses a name of a number to its numeric equivalent.
  96. * @param value - The value to parse.
  97. * @param options - Options to use when parsing a name of a number to its numeric equivalent.
  98. * @returns AllowedValue The numeric equivalent of the value.
  99. */
  100. export const parse = (value: string, options = {} as ParseOptions) => {
  101. const { system = enUS.shortCount, type = 'string' } = options;
  102. const tokens = system.tokenize(value);
  103. const groups = system.parseGroups(tokens);
  104. const stringValue = system.combineGroups(groups);
  105. switch (type) {
  106. case 'number': {
  107. // Precision might be lost here. Use bigint when not using fractional parts.
  108. if (stringValue.includes(EXPONENT_DELIMITER)) {
  109. const { exponent, integer } = extractExponentialComponents(stringValue);
  110. const exponentValue = Number(exponent);
  111. const integerValue = Number(integer);
  112. const [maxSafeIntegerSignificand, maxSafeIntegerExponent] = Number.MAX_SAFE_INTEGER.toExponential().split('e');
  113. if (
  114. exponentValue >= Number(maxSafeIntegerExponent)
  115. && integerValue >= Math.floor(Number(maxSafeIntegerSignificand))
  116. ) {
  117. // greater than Number.MAX_SAFE_INTEGER
  118. const logger = console;
  119. logger.warn(`Value too large to be produced as number: ${value}`);
  120. logger.warn('Falling back to string...');
  121. return stringValue;
  122. }
  123. const [epsilonSignificand, epsilonExponent] = Number.EPSILON.toExponential().split('e');
  124. if (
  125. exponentValue <= Number(epsilonExponent)
  126. && integerValue <= Math.floor(Number(epsilonSignificand))
  127. ) {
  128. // smaller than Number.EPSILON
  129. const logger = console;
  130. logger.warn(`Value too small to be produced as number: ${value}`);
  131. logger.warn('Falling back to string...');
  132. return stringValue;
  133. }
  134. }
  135. return Number(stringValue);
  136. } case 'bigint': {
  137. const normalizedNumberString = exponentialToNumberString(numberToExponential(stringValue));
  138. try {
  139. return BigInt(normalizedNumberString);
  140. } catch {
  141. const logger = console;
  142. logger.warn(`Value too long to be produced as bigint: ${value}`);
  143. logger.warn('Falling back to string...');
  144. }
  145. return stringValue;
  146. } default:
  147. break;
  148. }
  149. return stringValue;
  150. };