|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- /**
- * Valid values that can be converted to exponential notation.
- */
- export type ValidValue = string | number | bigint;
-
- /**
- * Options to use when converting a number to exponential notation.
- */
- export interface NumberToExponentialOptions {
- /**
- * The decimal point character to use. Defaults to ".".
- */
- decimalPoint?: string;
- /**
- * The grouping symbol to use. Defaults to ",".
- */
- groupingSymbol?: string;
- /**
- * Exponent character to use. Defaults to "e".
- */
- exponentDelimiter?: string;
- }
-
- /**
- * Extracts the integer, fractional, and exponent components of a string in exponential notation.
- * @param value - The string value to extract components from.
- * @param options - Options to use when extracting components.
- */
- export const extractExponentialComponents = (value: string, options = {} as NumberToExponentialOptions) => {
- const {
- decimalPoint = '.',
- groupingSymbol = ',',
- exponentDelimiter = 'e',
- } = options;
- const valueWithoutGroupingSymbols = value.replace(new RegExp(`${groupingSymbol}`, 'g'), '');
- const exponentDelimiterIndex = valueWithoutGroupingSymbols.indexOf(exponentDelimiter);
-
- if (exponentDelimiterIndex < 0) {
- // We force the value to have decimal point so that we can extract the integer and fractional
- // components.
- const stringValueWithDecimal = valueWithoutGroupingSymbols.includes(decimalPoint)
- ? valueWithoutGroupingSymbols
- : `${valueWithoutGroupingSymbols}${decimalPoint}0`;
- const [integerRaw, fractionalRaw] = stringValueWithDecimal.split(decimalPoint);
- const integer = integerRaw.replace(/^0+/g, '');
- const exponentValue = BigInt(integer.length - 1);
- return {
- integer,
- exponent: exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`,
- fractional: fractionalRaw.replace(/0+$/g, ''),
- };
- }
-
- if (exponentDelimiterIndex !== valueWithoutGroupingSymbols.lastIndexOf(exponentDelimiter)) {
- throw new TypeError('Value must not contain more than one exponent character');
- }
-
- const [base, exponentRaw] = valueWithoutGroupingSymbols.split(exponentDelimiter);
- const [integerRaw, fractionalRaw = ''] = base.split(decimalPoint);
- const integerWithoutZeroes = integerRaw.replace(/^0+/g, '');
- const integer = integerWithoutZeroes[0] ?? '0';
- const extraIntegerDigits = integerWithoutZeroes.slice(1);
- const fractional = `${extraIntegerDigits}${fractionalRaw.replace(/0+$/g, '')}`;
- const exponentValue = BigInt(exponentRaw) + BigInt(extraIntegerDigits.length);
- return {
- integer,
- exponent: exponentValue < 0 ? exponentValue.toString() : `+${exponentValue}`,
- fractional,
- };
- }
-
- /**
- * Converts a numeric value to a string in exponential notation. Supports numbers of all types.
- * @param value - The value to convert.
- * @param options - Options to use when extracting components.
- */
- export const numberToExponential = (value: ValidValue, options = {} as NumberToExponentialOptions): string => {
- const stringValueRaw = value as unknown
-
- if (typeof stringValueRaw === 'bigint' || typeof stringValueRaw === 'number') {
- return numberToExponential(stringValueRaw.toString(), options);
- }
-
- if (typeof stringValueRaw !== 'string') {
- throw new TypeError(`Value must be a string, number, or bigint. Received: ${typeof stringValueRaw}`);
- }
-
- if (stringValueRaw.startsWith('-')) {
- return `-${numberToExponential(stringValueRaw.slice(1), options)}}`;
- }
-
- const {
- decimalPoint = '.',
- groupingSymbol = ',',
- exponentDelimiter = 'e',
- } = options;
-
- const stringValue = stringValueRaw
- .replace(new RegExp(`${groupingSymbol}`, 'g'), '')
- .toLowerCase()
- .replace(/\s/g, '');
-
- const {
- integer,
- fractional,
- exponent,
- } = extractExponentialComponents(stringValue, options);
-
- const significantDigits = `${integer}${fractional}`;
- if (significantDigits.length === 0) {
- // We copy the behavior from `Number.prototype.toExponential` here.
- return `0${exponentDelimiter}+0`;
- }
-
- const significandInteger = significantDigits[0];
- const significandFractional = significantDigits.slice(1).replace(/0+$/g, '');
-
- if (significandFractional.length === 0) {
- return `${significandInteger}${exponentDelimiter}${exponent}`;
- }
-
- return `${significandInteger}${decimalPoint}${significandFractional}${exponentDelimiter}${exponent}`;
- };
|