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.
 
 

130 lines
4.2 KiB

  1. /**
  2. * Valid values that can be converted to exponential notation.
  3. */
  4. export type ValidValue = string | number | bigint;
  5. /**
  6. * Options to use when converting a number to exponential notation.
  7. */
  8. export interface NumberToExponentialOptions {
  9. /**
  10. * The decimal point character to use. Defaults to ".".
  11. */
  12. decimalPoint?: string;
  13. /**
  14. * The grouping symbol to use. Defaults to ",".
  15. */
  16. groupingSymbol?: string;
  17. /**
  18. * Exponent character to use. Defaults to "e".
  19. */
  20. exponentDelimiter?: string;
  21. }
  22. /**
  23. * Extracts the integer, fractional, and exponent components of a string in exponential notation.
  24. * @param value - The string value to extract components from.
  25. * @param options - Options to use when extracting components.
  26. */
  27. export const extractExponentialComponents = (
  28. value: string,
  29. options = {} as NumberToExponentialOptions,
  30. ) => {
  31. const {
  32. decimalPoint = '.',
  33. groupingSymbol = ',',
  34. exponentDelimiter = 'e',
  35. } = options;
  36. const valueWithoutGroupingSymbols = value.replace(new RegExp(`${groupingSymbol}`, 'g'), '');
  37. const exponentDelimiterIndex = valueWithoutGroupingSymbols.indexOf(exponentDelimiter);
  38. if (exponentDelimiterIndex < 0) {
  39. // We force the value to have decimal point so that we can extract the integer and fractional
  40. // components.
  41. const stringValueWithDecimal = valueWithoutGroupingSymbols.includes(decimalPoint)
  42. ? valueWithoutGroupingSymbols
  43. : `${valueWithoutGroupingSymbols}${decimalPoint}0`;
  44. const [integerRaw, fractionalRaw] = stringValueWithDecimal.split(decimalPoint);
  45. const integer = integerRaw.replace(/^0+/g, '');
  46. const exponentValue = BigInt(integer.length - 1);
  47. return {
  48. integer,
  49. exponent: exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`,
  50. fractional: fractionalRaw.replace(/0+$/g, ''),
  51. };
  52. }
  53. if (exponentDelimiterIndex !== valueWithoutGroupingSymbols.lastIndexOf(exponentDelimiter)) {
  54. throw new TypeError('Value must not contain more than one exponent character');
  55. }
  56. const [base, exponentRaw] = valueWithoutGroupingSymbols.split(exponentDelimiter);
  57. const [integerRaw, fractionalRaw = ''] = base.split(decimalPoint);
  58. const integerWithoutZeroes = integerRaw.replace(/^0+/g, '');
  59. const integer = integerWithoutZeroes[0] ?? '0';
  60. const extraIntegerDigits = integerWithoutZeroes.slice(1);
  61. const fractional = `${extraIntegerDigits}${fractionalRaw.replace(/0+$/g, '')}`;
  62. const exponentValue = BigInt(exponentRaw) + BigInt(extraIntegerDigits.length);
  63. return {
  64. integer,
  65. exponent: exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`,
  66. fractional,
  67. };
  68. };
  69. /**
  70. * Converts a numeric value to a string in exponential notation. Supports numbers of all types.
  71. * @param value - The value to convert.
  72. * @param options - Options to use when extracting components.
  73. */
  74. export const numberToExponential = (
  75. value: ValidValue,
  76. options = {} as NumberToExponentialOptions,
  77. ): string => {
  78. const stringValueRaw = value as unknown;
  79. if (typeof stringValueRaw === 'bigint' || typeof stringValueRaw === 'number') {
  80. return numberToExponential(stringValueRaw.toString(), options);
  81. }
  82. if (typeof stringValueRaw !== 'string') {
  83. throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof stringValueRaw}`);
  84. }
  85. if (stringValueRaw.startsWith('-')) {
  86. return `-${numberToExponential(stringValueRaw.slice(1), options)}}`;
  87. }
  88. const {
  89. decimalPoint = '.',
  90. groupingSymbol = ',',
  91. exponentDelimiter = 'e',
  92. } = options;
  93. const stringValue = stringValueRaw
  94. .replace(new RegExp(`${groupingSymbol}`, 'g'), '')
  95. .toLowerCase()
  96. .replace(/\s/g, '');
  97. const {
  98. integer,
  99. fractional,
  100. exponent,
  101. } = extractExponentialComponents(stringValue, options);
  102. const significantDigits = `${integer}${fractional}`;
  103. if (significantDigits.length === 0) {
  104. // We copy the behavior from `Number.prototype.toExponential` here.
  105. return `0${exponentDelimiter}+0`;
  106. }
  107. const significandInteger = significantDigits[0];
  108. const significandFractional = significantDigits.slice(1).replace(/0+$/g, '');
  109. if (significandFractional.length === 0) {
  110. return `${significandInteger}${exponentDelimiter}${exponent}`;
  111. }
  112. return `${significandInteger}${decimalPoint}${significandFractional}${exponentDelimiter}${exponent}`;
  113. };