Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

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