Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

170 行
5.0 KiB

  1. import { enUS } from './systems';
  2. import {
  3. ALLOWED_PARSE_RESULT_TYPES,
  4. AllowedValue,
  5. NumberNameSystem,
  6. ParseResultType,
  7. } from './common';
  8. import { exponentialToNumberString, extractExponentialComponents, numberToExponential } from './exponent';
  9. /**
  10. * Negative symbol.
  11. */
  12. const NEGATIVE_SYMBOL = '-' as const;
  13. /**
  14. * Exponent delimiter.
  15. */
  16. const EXPONENT_DELIMITER = 'e' as const;
  17. /**
  18. * Options to use when converting a value to a string.
  19. */
  20. export interface StringifyOptions<
  21. TStringifyGroupsOptions extends object,
  22. TMergeTokensOptions extends object,
  23. > {
  24. /**
  25. * The system to use when converting a value to a string.
  26. *
  27. * Defaults to en-US (American short count).
  28. */
  29. system?: NumberNameSystem;
  30. /**
  31. * Options to use when stringifying a single group.
  32. */
  33. stringifyGroupsOptions?: TStringifyGroupsOptions;
  34. /**
  35. * Options to use when merging tokens.
  36. */
  37. mergeTokensOptions?: TMergeTokensOptions;
  38. }
  39. /**
  40. * Converts a numeric value to its name.
  41. * @param value - The value to convert.
  42. * @param options - Options to use when converting a value to its name.
  43. * @returns string The name of the value.
  44. */
  45. export const stringify = <
  46. TStringifyGroupsOptions extends object,
  47. TMergeTokensOptions extends object
  48. >
  49. (
  50. value: AllowedValue,
  51. options = {} as StringifyOptions<TStringifyGroupsOptions, TMergeTokensOptions>,
  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. if (typeof value === 'number' && Number.isNaN(value)) {
  60. return 'NaN';
  61. }
  62. const valueStr = value.toString().replace(/\s/g, '');
  63. const { system = enUS.shortCount, stringifyGroupsOptions, mergeTokensOptions } = options;
  64. if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
  65. return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
  66. }
  67. const groups = system.splitIntoGroups(valueStr);
  68. const groupNames = system.stringifyGroups(groups, stringifyGroupsOptions);
  69. return system.mergeTokens(groupNames, mergeTokensOptions);
  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?: NumberNameSystem;
  81. /**
  82. * The type to parse the value as.
  83. */
  84. type?: ParseResultType;
  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.shortCount, type: typeRaw = 'string' } = options;
  94. const type = typeRaw.trim().toLowerCase() as ParseResultType;
  95. if (!((ALLOWED_PARSE_RESULT_TYPES as unknown as string[]).includes(type))) {
  96. throw new TypeError(`Return type must be a string, number, or bigint. Received: ${type}`);
  97. }
  98. if (value.trim().toLowerCase() === 'nan') {
  99. return type === 'number' ? NaN : 'NaN';
  100. }
  101. const tokens = system.tokenize(value);
  102. const { groups, negative } = system.parseGroups(tokens);
  103. const stringValue = system.combineGroups(groups, negative);
  104. switch (type) {
  105. case 'number': {
  106. // Precision might be lost here. Use bigint when not using fractional parts.
  107. if (stringValue.includes(EXPONENT_DELIMITER)) {
  108. const { exponent, integer } = extractExponentialComponents(stringValue);
  109. const exponentValue = Number(exponent);
  110. const integerValue = Number(integer);
  111. // max safe integer: 9.007199254740991e+15
  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. // epsilon: 2.220446049250313e-16
  124. const [epsilonSignificand, epsilonExponent] = Number.EPSILON.toExponential().split('e');
  125. if (
  126. exponentValue <= Number(epsilonExponent)
  127. && integerValue <= Math.floor(Number(epsilonSignificand))
  128. ) {
  129. // smaller than Number.EPSILON
  130. const logger = console;
  131. logger.warn(`Value too small to be produced as number: ${value}`);
  132. logger.warn('Falling back to string...');
  133. return stringValue;
  134. }
  135. }
  136. return Number(stringValue);
  137. } case 'bigint': {
  138. const normalizedNumberString = exponentialToNumberString(numberToExponential(stringValue));
  139. try {
  140. return BigInt(normalizedNumberString);
  141. } catch {
  142. const logger = console;
  143. logger.warn(`Value too long to be produced as bigint: ${value}`);
  144. logger.warn('Falling back to string...');
  145. }
  146. return stringValue;
  147. } default:
  148. break;
  149. }
  150. return stringValue;
  151. };