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.
 
 

206 lines
6.1 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. * The exponent character to use. Defaults to "e".
  19. */
  20. exponentDelimiter?: string;
  21. }
  22. /**
  23. * Default decimal point character.
  24. */
  25. const DEFAULT_DECIMAL_POINT = '.' as const;
  26. /**
  27. * Default grouping symbol.
  28. */
  29. const DEFAULT_GROUPING_SYMBOL = ',' as const;
  30. /**
  31. * Default exponent character.
  32. */
  33. const DEFAULT_EXPONENT_DELIMITER = 'e' as const;
  34. /**
  35. * Default positive symbol.
  36. */
  37. const DEFAULT_POSITIVE_SYMBOL = '+' as const;
  38. /**
  39. * Default negative symbol.
  40. */
  41. const DEFAULT_NEGATIVE_SYMBOL = '-' as const;
  42. /**
  43. * Forces a value to have a decimal point.
  44. * @param value - The value to force a decimal point on.
  45. * @param decimalPoint - The decimal point character to use.
  46. * @returns string The value with a decimal point.
  47. */
  48. const forceDecimalPoint = (value: string, decimalPoint: string) => (
  49. value.includes(decimalPoint)
  50. ? value
  51. : `${value}${decimalPoint}0`
  52. );
  53. /**
  54. * Error thrown when a value is not in exponential notation.
  55. */
  56. export class InvalidFormatError extends TypeError {
  57. constructor(value: string) {
  58. super(`Value must be in exponential notation. Received: ${value}`);
  59. }
  60. }
  61. /**
  62. * Error thrown when a value is not a string, number, or bigint.
  63. */
  64. export class InvalidValueTypeError extends TypeError {
  65. constructor(value: unknown) {
  66. super(`Value must be a string, number, or bigint. Received: ${typeof value}`);
  67. }
  68. }
  69. /**
  70. * Forces a number to have a sign.
  71. * @param value - The value to force a sign on.
  72. * @returns string The value with a sign.
  73. */
  74. const forceNumberSign = (value: number | bigint) => {
  75. const isExponentNegative = value < 0;
  76. const exponentValueAbs = isExponentNegative ? -value : value;
  77. const exponentSign = isExponentNegative ? DEFAULT_NEGATIVE_SYMBOL : DEFAULT_POSITIVE_SYMBOL;
  78. return `${exponentSign}${exponentValueAbs}`;
  79. };
  80. interface ExponentialComponents {
  81. integer: string;
  82. fractional: string;
  83. exponent: string;
  84. }
  85. /**
  86. * Extracts the integer, fractional, and exponent components of a string in exponential notation.
  87. * @param value - The string value to extract components from.
  88. * @param options - Options to use when extracting components.
  89. * @returns ExponentialComponents The extracted components.
  90. */
  91. export const extractExponentialComponents = (
  92. value: string,
  93. options = {} as NumberToExponentialOptions,
  94. ): ExponentialComponents => {
  95. const {
  96. decimalPoint = DEFAULT_DECIMAL_POINT,
  97. groupingSymbol = DEFAULT_GROUPING_SYMBOL,
  98. exponentDelimiter = DEFAULT_EXPONENT_DELIMITER,
  99. } = options;
  100. const valueWithoutGroupingSymbols = value.replace(new RegExp(`${groupingSymbol}`, 'g'), '');
  101. const exponentDelimiterIndex = valueWithoutGroupingSymbols.indexOf(exponentDelimiter);
  102. if (exponentDelimiterIndex < 0) {
  103. // We force the value to have decimal point so that we can extract the integer and fractional
  104. // components.
  105. const stringValueWithDecimal = forceDecimalPoint(valueWithoutGroupingSymbols, decimalPoint);
  106. const [integerRaw, fractionalRaw] = stringValueWithDecimal.split(decimalPoint);
  107. const integer = integerRaw.replace(/^0+/g, '');
  108. const fractional = fractionalRaw.replace(/0+$/g, '');
  109. const exponentValue = BigInt(integer.length - 1);
  110. return {
  111. integer,
  112. exponent: forceNumberSign(exponentValue),
  113. fractional,
  114. };
  115. }
  116. if (exponentDelimiterIndex !== valueWithoutGroupingSymbols.lastIndexOf(exponentDelimiter)) {
  117. throw new InvalidFormatError(value);
  118. }
  119. const [base, exponentRaw] = valueWithoutGroupingSymbols.split(exponentDelimiter);
  120. const [integerRaw, fractionalRaw = ''] = base.split(decimalPoint);
  121. const integerWithoutZeroes = integerRaw.replace(/^0+/g, '');
  122. const integer = integerWithoutZeroes[0] ?? '0';
  123. const extraIntegerDigits = integerWithoutZeroes.slice(1);
  124. const fractional = `${extraIntegerDigits}${fractionalRaw.replace(/0+$/g, '')}`;
  125. const exponentValue = BigInt(exponentRaw) + BigInt(extraIntegerDigits.length);
  126. return {
  127. integer,
  128. exponent: forceNumberSign(exponentValue),
  129. fractional,
  130. };
  131. };
  132. /**
  133. * Converts a numeric value to a string in exponential notation. Supports numbers of all types.
  134. * @param value - The value to convert.
  135. * @param options - Options to use when extracting components.
  136. * @returns string The value in exponential notation.
  137. */
  138. export const numberToExponential = (
  139. value: ValidValue,
  140. options = {} as NumberToExponentialOptions,
  141. ): string => {
  142. const valueRaw = value as unknown;
  143. if (typeof valueRaw === 'bigint' || typeof valueRaw === 'number') {
  144. return numberToExponential(valueRaw.toString(), options);
  145. }
  146. if (typeof valueRaw !== 'string') {
  147. throw new InvalidValueTypeError(valueRaw);
  148. }
  149. if (valueRaw.startsWith(DEFAULT_NEGATIVE_SYMBOL)) {
  150. return `${DEFAULT_NEGATIVE_SYMBOL}${numberToExponential(valueRaw.slice(DEFAULT_NEGATIVE_SYMBOL.length), options)}}`;
  151. }
  152. const {
  153. decimalPoint = DEFAULT_DECIMAL_POINT,
  154. groupingSymbol = DEFAULT_GROUPING_SYMBOL,
  155. exponentDelimiter = DEFAULT_EXPONENT_DELIMITER,
  156. } = options;
  157. // Remove invalid characters.
  158. // We also remove the grouping symbols in case they come from human input.
  159. const stringValue = valueRaw
  160. .replace(new RegExp(`${groupingSymbol}`, 'g'), '')
  161. .toLowerCase()
  162. .replace(/\s/g, '');
  163. const {
  164. integer,
  165. fractional,
  166. exponent,
  167. } = extractExponentialComponents(stringValue, options);
  168. const significantDigits = `${integer}${fractional}`;
  169. if (significantDigits.length === 0) {
  170. // We copy the behavior from `Number.prototype.toExponential` here.
  171. return `0${exponentDelimiter}${DEFAULT_POSITIVE_SYMBOL}0`;
  172. }
  173. const significandInteger = significantDigits[0];
  174. const significandFractional = significantDigits.slice(1).replace(/0+$/g, '');
  175. if (significandFractional.length === 0) {
  176. return `${significandInteger}${exponentDelimiter}${exponent}`;
  177. }
  178. return `${significandInteger}${decimalPoint}${significandFractional}${exponentDelimiter}${exponent}`;
  179. };