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.
 
 

118 lines
3.0 KiB

  1. import { enUS } from './systems';
  2. import { StringifySystem } from './common';
  3. import { exponentialToNumberString } from './exponent';
  4. /**
  5. * Negative symbol.
  6. */
  7. const NEGATIVE_SYMBOL = '-' as const;
  8. /**
  9. * Allowed value type for {@link stringify}.
  10. */
  11. type AllowedValue = string | number | bigint;
  12. /**
  13. * Array of allowed types for {@link parse}.
  14. */
  15. const ALLOWED_PARSE_RESULT_TYPES = [
  16. 'string',
  17. 'number',
  18. 'bigint',
  19. ] as const;
  20. /**
  21. * Allowed type for {@link parse}.
  22. */
  23. type ParseResult = typeof ALLOWED_PARSE_RESULT_TYPES[number];
  24. /**
  25. * Options to use when converting a value to a string.
  26. */
  27. export interface StringifyOptions<TMakeGroupOptions extends object = object> {
  28. /**
  29. * The system to use when converting a value to a string.
  30. *
  31. * Defaults to en-US (American short count).
  32. */
  33. system?: StringifySystem;
  34. /**
  35. * Options to use when making a group. This is used to override the default options for a group.
  36. */
  37. makeGroupOptions?: TMakeGroupOptions;
  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. value: AllowedValue,
  47. options = {} as StringifyOptions,
  48. ): string => {
  49. if (!(
  50. (ALLOWED_PARSE_RESULT_TYPES as unknown as string[])
  51. .includes(typeof (value as unknown))
  52. )) {
  53. throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof value}`);
  54. }
  55. const valueStr = value.toString().replace(/\s/g, '');
  56. const { system = enUS, makeGroupOptions } = options;
  57. if (valueStr.startsWith(NEGATIVE_SYMBOL)) {
  58. return system.makeNegative(stringify(valueStr.slice(NEGATIVE_SYMBOL.length), options));
  59. }
  60. const groups = system.group(valueStr);
  61. const groupNames = system.makeGroups(
  62. groups,
  63. makeGroupOptions,
  64. );
  65. return system.finalize(groupNames);
  66. };
  67. /**
  68. * Options to use when parsing a name of a number to its numeric equivalent.
  69. */
  70. export interface ParseOptions {
  71. /**
  72. * The system to use when parsing a name of a number to its numeric equivalent.
  73. *
  74. * Defaults to en-US (American short count).
  75. */
  76. system?: StringifySystem;
  77. /**
  78. * The type to parse the value as.
  79. */
  80. type?: ParseResult;
  81. }
  82. /**
  83. * Parses a name of a number to its numeric equivalent.
  84. * @param value - The value to parse.
  85. * @param options - Options to use when parsing a name of a number to its numeric equivalent.
  86. * @returns AllowedValue The numeric equivalent of the value.
  87. */
  88. export const parse = (value: string, options = {} as ParseOptions) => {
  89. const { system = enUS, type = 'string' } = options;
  90. const tokens = system.tokenize(value);
  91. const groups = system.parseGroups(tokens);
  92. const stringValue = system.combineGroups(groups);
  93. switch (type) {
  94. case 'number':
  95. // Precision might be lost here. Use bigint when not using fractional parts.
  96. return Number(stringValue);
  97. case 'bigint':
  98. return BigInt(exponentialToNumberString(stringValue));
  99. default:
  100. break;
  101. }
  102. return stringValue;
  103. };