Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

184 строки
5.3 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. export 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. if (typeof value === 'number' && Number.isNaN(value)) {
  71. return 'NaN';
  72. }
  73. const valueStr = value.toString().replace(/\s/g, '');
  74. const { system = enUS.shortCount, stringifyGroupsOptions, mergeTokensOptions } = options;
  75. if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
  76. return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
  77. }
  78. const groups = system.splitIntoGroups(valueStr);
  79. const groupNames = system.stringifyGroups(groups, stringifyGroupsOptions);
  80. return system.mergeTokens(groupNames, mergeTokensOptions);
  81. };
  82. /**
  83. * Options to use when parsing a name of a number to its numeric equivalent.
  84. */
  85. export interface ParseOptions {
  86. /**
  87. * The system to use when parsing a name of a number to its numeric equivalent.
  88. *
  89. * Defaults to en-US (American short count).
  90. */
  91. system?: NumberNameSystem;
  92. /**
  93. * The type to parse the value as.
  94. */
  95. type?: ParseResult;
  96. }
  97. /**
  98. * Parses a name of a number to its numeric equivalent.
  99. * @param value - The value to parse.
  100. * @param options - Options to use when parsing a name of a number to its numeric equivalent.
  101. * @returns AllowedValue The numeric equivalent of the value.
  102. */
  103. export const parse = (value: string, options = {} as ParseOptions) => {
  104. const { system = enUS.shortCount, type: typeRaw = 'string' } = options;
  105. const type = typeRaw.trim().toLowerCase() as ParseResult;
  106. if (!((ALLOWED_PARSE_RESULT_TYPES as unknown as string[]).includes(type))) {
  107. throw new TypeError(`Return type must be a string, number, or bigint. Received: ${type}`);
  108. }
  109. if (value.trim().toLowerCase() === 'nan') {
  110. return type === 'number' ? NaN : 'NaN';
  111. }
  112. const tokens = system.tokenize(value);
  113. const { groups, negative } = system.parseGroups(tokens);
  114. const stringValue = system.combineGroups(groups, negative);
  115. switch (type) {
  116. case 'number': {
  117. // Precision might be lost here. Use bigint when not using fractional parts.
  118. if (stringValue.includes(EXPONENT_DELIMITER)) {
  119. const { exponent, integer } = extractExponentialComponents(stringValue);
  120. const exponentValue = Number(exponent);
  121. const integerValue = Number(integer);
  122. // max safe integer: 9.007199254740991e+15
  123. const [maxSafeIntegerSignificand, maxSafeIntegerExponent] = Number.MAX_SAFE_INTEGER.toExponential().split('e');
  124. if (
  125. exponentValue >= Number(maxSafeIntegerExponent)
  126. && integerValue >= Math.floor(Number(maxSafeIntegerSignificand))
  127. ) {
  128. // greater than Number.MAX_SAFE_INTEGER
  129. const logger = console;
  130. logger.warn(`Value too large to be produced as number: ${value}`);
  131. logger.warn('Falling back to string...');
  132. return stringValue;
  133. }
  134. // epsilon: 2.220446049250313e-16
  135. const [epsilonSignificand, epsilonExponent] = Number.EPSILON.toExponential().split('e');
  136. if (
  137. exponentValue <= Number(epsilonExponent)
  138. && integerValue <= Math.floor(Number(epsilonSignificand))
  139. ) {
  140. // smaller than Number.EPSILON
  141. const logger = console;
  142. logger.warn(`Value too small to be produced as number: ${value}`);
  143. logger.warn('Falling back to string...');
  144. return stringValue;
  145. }
  146. }
  147. return Number(stringValue);
  148. } case 'bigint': {
  149. const normalizedNumberString = exponentialToNumberString(numberToExponential(stringValue));
  150. try {
  151. return BigInt(normalizedNumberString);
  152. } catch {
  153. const logger = console;
  154. logger.warn(`Value too long to be produced as bigint: ${value}`);
  155. logger.warn('Falling back to string...');
  156. }
  157. return stringValue;
  158. } default:
  159. break;
  160. }
  161. return stringValue;
  162. };