/** * 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}`; };