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.
 
 

114 lines
2.8 KiB

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