Gets the name of a number, even if it's stupidly big. Supersedes TheoryOfNekomata/number-name.
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

268 líneas
8.2 KiB

  1. import { AllowedValue } from './common';
  2. interface BaseOptions {
  3. /**
  4. * The decimal point character to use. Defaults to ".".
  5. */
  6. decimalPoint?: string;
  7. /**
  8. * The grouping symbol to use. Defaults to ",".
  9. */
  10. groupingSymbol?: string;
  11. /**
  12. * The exponent character to use. Defaults to "e".
  13. */
  14. exponentDelimiter?: string;
  15. }
  16. /**
  17. * Options to use when converting a number to exponential notation.
  18. */
  19. export type NumberToExponentialOptions = BaseOptions;
  20. /**
  21. * Options to use when converting exponential notation to a number string.
  22. */
  23. export interface ExponentialToNumberStringOptions extends BaseOptions {
  24. /**
  25. * The maximum length to represent the return value else the input will be preserved.
  26. */
  27. maxLength?: number;
  28. }
  29. /**
  30. * Default decimal point character.
  31. */
  32. const DEFAULT_DECIMAL_POINT = '.' as const;
  33. /**
  34. * Default grouping symbol.
  35. */
  36. const DEFAULT_GROUPING_SYMBOL = ',' as const;
  37. /**
  38. * Default exponent character.
  39. */
  40. const DEFAULT_EXPONENT_DELIMITER = 'e' as const;
  41. /**
  42. * Default positive symbol.
  43. */
  44. const DEFAULT_POSITIVE_SYMBOL = '+' as const;
  45. /**
  46. * Default negative symbol.
  47. */
  48. const DEFAULT_NEGATIVE_SYMBOL = '-' as const;
  49. /**
  50. * Forces a value to have a decimal point.
  51. * @param value - The value to force a decimal point on.
  52. * @param decimalPoint - The decimal point character to use.
  53. * @returns string The value with a decimal point.
  54. */
  55. const forceDecimalPoint = (value: string, decimalPoint: string) => (
  56. value.includes(decimalPoint)
  57. ? value
  58. : `${value}${decimalPoint}0`
  59. );
  60. /**
  61. * Error thrown when a value is not in exponential notation.
  62. */
  63. export class InvalidFormatError extends TypeError {
  64. constructor(value: string) {
  65. super(`Value must be in exponential notation. Received: ${value}`);
  66. }
  67. }
  68. /**
  69. * Error thrown when a value is not a string, number, or bigint.
  70. */
  71. export class InvalidValueTypeError extends TypeError {
  72. constructor(value: unknown) {
  73. super(`Value must be a string, number, or bigint. Received: ${typeof value}`);
  74. }
  75. }
  76. /**
  77. * Forces a number to have a sign.
  78. * @param value - The value to force a sign on.
  79. * @returns string The value with a sign.
  80. */
  81. const forceNumberSign = (value: bigint) => {
  82. const isExponentNegative = value < 0;
  83. const exponentValueAbs = isExponentNegative ? -value : value;
  84. const exponentSign = isExponentNegative ? DEFAULT_NEGATIVE_SYMBOL : DEFAULT_POSITIVE_SYMBOL;
  85. return `${exponentSign}${exponentValueAbs}`;
  86. };
  87. interface ExponentialComponents {
  88. integer: string;
  89. fractional: string;
  90. exponent: string;
  91. }
  92. /**
  93. * Extracts the integer, fractional, and exponent components of a string in exponential notation.
  94. * @param value - The string value to extract components from.
  95. * @param options - Options to use when extracting components.
  96. * @returns ExponentialComponents The extracted components.
  97. */
  98. export const extractExponentialComponents = (
  99. value: string,
  100. options = {} as BaseOptions,
  101. ): ExponentialComponents => {
  102. const {
  103. decimalPoint = DEFAULT_DECIMAL_POINT,
  104. groupingSymbol = DEFAULT_GROUPING_SYMBOL,
  105. exponentDelimiter = DEFAULT_EXPONENT_DELIMITER,
  106. } = options;
  107. const valueWithoutGroupingSymbols = value.replace(new RegExp(`${groupingSymbol}`, 'g'), '');
  108. const exponentDelimiterIndex = valueWithoutGroupingSymbols.indexOf(exponentDelimiter);
  109. if (exponentDelimiterIndex < 0) {
  110. // We force the value to have decimal point so that we can extract the integer and fractional
  111. // components.
  112. const stringValueWithDecimal = forceDecimalPoint(valueWithoutGroupingSymbols, decimalPoint);
  113. const [integerRaw, fractionalRaw] = stringValueWithDecimal.split(decimalPoint);
  114. const integer = integerRaw.replace(/^0+/, '');
  115. const fractional = fractionalRaw.replace(/0+$/, '');
  116. const exponentValue = BigInt(integer.length - 1);
  117. return {
  118. integer,
  119. exponent: forceNumberSign(exponentValue),
  120. fractional,
  121. };
  122. }
  123. if (exponentDelimiterIndex !== valueWithoutGroupingSymbols.lastIndexOf(exponentDelimiter)) {
  124. throw new InvalidFormatError(value);
  125. }
  126. const [base, exponentRaw] = valueWithoutGroupingSymbols.split(exponentDelimiter);
  127. const [integerRaw, fractionalRaw = ''] = base.split(decimalPoint);
  128. const integerWithoutZeroes = integerRaw.replace(/^0+/, '');
  129. const integer = integerWithoutZeroes[0] ?? '0';
  130. const extraIntegerDigits = integerWithoutZeroes.slice(1);
  131. const fractional = `${extraIntegerDigits}${fractionalRaw.replace(/0+$/, '')}`;
  132. const exponentValue = BigInt(exponentRaw) + BigInt(extraIntegerDigits.length);
  133. return {
  134. integer,
  135. exponent: forceNumberSign(exponentValue),
  136. fractional,
  137. };
  138. };
  139. /**
  140. * Converts a numeric value to a string in exponential notation.
  141. * @param value - The value to convert.
  142. * @param options - Options to use when extracting components.
  143. * @returns string The value in exponential notation.
  144. */
  145. export const numberToExponential = (
  146. value: AllowedValue,
  147. options = {} as NumberToExponentialOptions,
  148. ): string => {
  149. const valueRaw = value as unknown;
  150. if (typeof valueRaw === 'bigint' || typeof valueRaw === 'number') {
  151. return numberToExponential(valueRaw.toString(), options);
  152. }
  153. if (typeof valueRaw !== 'string') {
  154. throw new InvalidValueTypeError(valueRaw);
  155. }
  156. if (valueRaw.startsWith(DEFAULT_NEGATIVE_SYMBOL)) {
  157. return `${DEFAULT_NEGATIVE_SYMBOL}${numberToExponential(valueRaw.slice(DEFAULT_NEGATIVE_SYMBOL.length), options)}}`;
  158. }
  159. const {
  160. decimalPoint = DEFAULT_DECIMAL_POINT,
  161. groupingSymbol = DEFAULT_GROUPING_SYMBOL,
  162. exponentDelimiter = DEFAULT_EXPONENT_DELIMITER,
  163. } = options;
  164. // Remove invalid characters.
  165. // We also remove the grouping symbols in case they come from human input.
  166. const stringValue = valueRaw
  167. .replace(new RegExp(`${groupingSymbol}`, 'g'), '')
  168. .toLowerCase()
  169. .replace(/\s/g, '');
  170. const {
  171. integer,
  172. fractional,
  173. exponent,
  174. } = extractExponentialComponents(stringValue, options);
  175. const significantDigits = `${integer}${fractional}`;
  176. if (significantDigits.length === 0) {
  177. // We copy the behavior from `Number.prototype.toExponential` here.
  178. return `0${exponentDelimiter}${DEFAULT_POSITIVE_SYMBOL}0`;
  179. }
  180. const significandInteger = significantDigits[0];
  181. const significandFractional = significantDigits.slice(1).replace(/0+$/, '');
  182. if (significandFractional.length === 0) {
  183. return `${significandInteger}${exponentDelimiter}${exponent}`;
  184. }
  185. return `${significandInteger}${decimalPoint}${significandFractional}${exponentDelimiter}${exponent}`;
  186. };
  187. /**
  188. * Converts a string in exponential notation (e.g. 1e+2) to a number string (e.g. 100).
  189. * @param exp - The number in exponential notation to convert.
  190. * @param options - Options to use when converting number strings.
  191. * @returns string The number string.
  192. */
  193. export const exponentialToNumberString = (
  194. exp: string,
  195. options = {} as ExponentialToNumberStringOptions,
  196. ) => {
  197. const {
  198. decimalPoint = DEFAULT_DECIMAL_POINT,
  199. maxLength = Number.MAX_SAFE_INTEGER,
  200. } = options;
  201. const { integer, fractional, exponent } = extractExponentialComponents(exp, options);
  202. const currentDecimalPointIndex = integer.length;
  203. const newDecimalPointIndex = currentDecimalPointIndex + Number(exponent);
  204. if (newDecimalPointIndex > maxLength) {
  205. // Integer part overflow
  206. //
  207. // Important to bail out as early as possible.
  208. throw new RangeError('Could not represent number in string form.');
  209. }
  210. const fractionalDigits = fractional.length > 0 ? fractional : '0';
  211. const significantDigits = `${integer}${fractionalDigits}`;
  212. if (significantDigits.length > maxLength) {
  213. // Digits overflow
  214. //
  215. // Should we choose to truncate instead of throw an exception?
  216. // Quite tricky when only the fractional part overflows.
  217. throw new RangeError('Could not represent number in string form.');
  218. }
  219. let newInteger: string;
  220. try {
  221. newInteger = significantDigits.slice(0, newDecimalPointIndex)
  222. .padEnd(newDecimalPointIndex, '0');
  223. } catch {
  224. // Error in padEnd(), shadow it.
  225. throw new RangeError('Could not represent number in string form.');
  226. }
  227. const newFractional = significantDigits.slice(newDecimalPointIndex).replace(/0+$/, '');
  228. if (newFractional.length === 0) {
  229. return newInteger;
  230. }
  231. if (newInteger.length + decimalPoint.length + newFractional.length > maxLength) {
  232. // Fractional part overflow
  233. //
  234. // Final length check.
  235. throw new RangeError('Could not represent number in string form.');
  236. }
  237. return `${newInteger}${decimalPoint}${newFractional}`;
  238. };