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.
 
 

165 lines
4.6 KiB

  1. import { enUS } from './systems';
  2. import { StringifySystem } 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. TMakeGroupOptions extends object = object,
  33. TFinalizeOptions extends object = 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?: StringifySystem;
  41. /**
  42. * Options to use when making a group. This is used to override the default options for a group.
  43. */
  44. makeGroupOptions?: TMakeGroupOptions;
  45. finalizeOptions?: TFinalizeOptions;
  46. }
  47. /**
  48. * Converts a numeric value to its name.
  49. * @param value - The value to convert.
  50. * @param options - Options to use when converting a value to its name.
  51. * @returns string The name of the value.
  52. */
  53. export const stringify = (
  54. value: AllowedValue,
  55. options = {} as StringifyOptions,
  56. ): string => {
  57. if (!(
  58. (ALLOWED_PARSE_RESULT_TYPES as unknown as string[])
  59. .includes(typeof (value as unknown))
  60. )) {
  61. throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof value}`);
  62. }
  63. const valueStr = value.toString().replace(/\s/g, '');
  64. const { system = enUS.shortCount, makeGroupOptions, finalizeOptions } = options;
  65. if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
  66. return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
  67. }
  68. const groups = system.group(valueStr);
  69. const groupNames = system.makeGroups(
  70. groups,
  71. makeGroupOptions,
  72. );
  73. return system.finalize(groupNames, finalizeOptions);
  74. };
  75. /**
  76. * Options to use when parsing a name of a number to its numeric equivalent.
  77. */
  78. export interface ParseOptions {
  79. /**
  80. * The system to use when parsing a name of a number to its numeric equivalent.
  81. *
  82. * Defaults to en-US (American short count).
  83. */
  84. system?: StringifySystem;
  85. /**
  86. * The type to parse the value as.
  87. */
  88. type?: ParseResult;
  89. }
  90. /**
  91. * Parses a name of a number to its numeric equivalent.
  92. * @param value - The value to parse.
  93. * @param options - Options to use when parsing a name of a number to its numeric equivalent.
  94. * @returns AllowedValue The numeric equivalent of the value.
  95. */
  96. export const parse = (value: string, options = {} as ParseOptions) => {
  97. const { system = enUS.shortCount, type = 'string' } = options;
  98. const tokens = system.tokenize(value);
  99. const groups = system.parseGroups(tokens);
  100. const stringValue = system.combineGroups(groups);
  101. switch (type) {
  102. case 'number': {
  103. // Precision might be lost here. Use bigint when not using fractional parts.
  104. if (stringValue.includes(EXPONENT_DELIMITER)) {
  105. const { exponent, integer } = extractExponentialComponents(stringValue);
  106. const exponentValue = Number(exponent);
  107. const integerValue = Number(integer);
  108. const [maxSafeIntegerSignificand, maxSafeIntegerExponent] = Number.MAX_SAFE_INTEGER.toExponential().split('e');
  109. if (
  110. exponentValue >= Number(maxSafeIntegerExponent)
  111. && integerValue >= Math.floor(Number(maxSafeIntegerSignificand))
  112. ) {
  113. // greater than Number.MAX_SAFE_INTEGER
  114. const logger = console;
  115. logger.warn(`Value too large to be produced as number: ${value}`);
  116. logger.warn('Falling back to string...');
  117. return stringValue;
  118. }
  119. const [epsilonSignificand, epsilonExponent] = Number.EPSILON.toExponential().split('e');
  120. if (
  121. exponentValue <= Number(epsilonExponent)
  122. && integerValue <= Math.floor(Number(epsilonSignificand))
  123. ) {
  124. // smaller than Number.EPSILON
  125. const logger = console;
  126. logger.warn(`Value too small to be produced as number: ${value}`);
  127. logger.warn('Falling back to string...');
  128. return stringValue;
  129. }
  130. }
  131. return Number(stringValue);
  132. } case 'bigint': {
  133. const normalizedNumberString = exponentialToNumberString(numberToExponential(stringValue));
  134. try {
  135. return BigInt(normalizedNumberString);
  136. } catch {
  137. const logger = console;
  138. logger.warn(`Value too long to be produced as bigint: ${value}`);
  139. logger.warn('Falling back to string...');
  140. }
  141. return stringValue;
  142. } default:
  143. break;
  144. }
  145. return stringValue;
  146. };