UNPKG

sigfig

Version:

Round a number to n significant figures.

240 lines (230 loc) 8.37 kB
'use strict'; /** * Return two parts array of exponential number * @param {number|string|Array} num * @return {string[]} */ function getExponentialParts(num) { return Array.isArray(num) ? num : String(num).split(/[eE]/); } /** * * @param {number|string|Array} num - number or array of its parts */ function isExponential(num) { const eParts = getExponentialParts(num); return !Number.isNaN(Number(eParts[1])); } /** * Converts exponential notation to a human readable string * @param {number|string|Array} num - number or array of its parts * @return {string} */ function fromExponential(num) { const eParts = getExponentialParts(num); if (!isExponential(eParts)) { return eParts[0]; } const sign = eParts[0][0] === '-' ? '-' : ''; const digits = eParts[0].replace(/^-/, ''); const digitsParts = digits.split('.'); const wholeDigits = digitsParts[0]; const fractionDigits = digitsParts[1] || ''; let e = Number(eParts[1]); if (e === 0) { return `${sign + wholeDigits}.${fractionDigits}`; } else if (e < 0) { // move dot to the left const countWholeAfterTransform = wholeDigits.length + e; if (countWholeAfterTransform > 0) { // transform whole to fraction const wholeDigitsAfterTransform = wholeDigits.substr(0, countWholeAfterTransform); const wholeDigitsTransformedToFraction = wholeDigits.substr(countWholeAfterTransform); return `${sign + wholeDigitsAfterTransform}.${wholeDigitsTransformedToFraction}${fractionDigits}`; } else { // not enough whole digits: prepend with fractional zeros // first e goes to dotted zero let zeros = '0.'; e = countWholeAfterTransform; while (e) { zeros += '0'; e += 1; } return sign + zeros + wholeDigits + fractionDigits; } } else { // move dot to the right const countFractionAfterTransform = fractionDigits.length - e; if (countFractionAfterTransform > 0) { // transform fraction to whole // countTransformedFractionToWhole = e const fractionDigitsAfterTransform = fractionDigits.substr(e); const fractionDigitsTransformedToWhole = fractionDigits.substr(0, e); return `${sign + wholeDigits + fractionDigitsTransformedToWhole}.${fractionDigitsAfterTransform}`; } else { // not enough fractions: append whole zeros let zerosCount = -countFractionAfterTransform; let zeros = ''; while (zerosCount) { zeros += '0'; zerosCount -= 1; } return sign + wholeDigits + fractionDigits + zeros; } } } function convertNumberToString(numberOrString) { let numberString = typeof numberOrString === 'number' ? numberOrString.toFixed(100) : numberOrString; if (Number.isNaN(Number(numberOrString))) { throw new TypeError(`${numberOrString} is not a number.`); } // Convert exponential to decimal format numberString = fromExponential(numberString); return numberString; } /** Returns a normalized representation of the number, which includes: - Leading zeros excluded - Always a decimal point. If the number is a whole number, the decimal point is at the end of the number (1234.) If the absolute value of the number is less than 1, then the leading zero is omitted (.1234) */ function normalizeNumberString(numberString) { // Remove the sign from the number const isNegative = numberString.startsWith('-'); if (numberString.startsWith('+') || numberString.startsWith('-')) { numberString = numberString.slice(1); } // Add a decimal to the end of the number if number doesn't have explicit decimal place (i.e. whole number) if (!numberString.includes('.')) { numberString += '.'; } // Remove leading zeros (zeros at the beginning) numberString = numberString.replace(/^0*(?!\.)/g, ''); // If the number is 0.abc, replace it with .abc if (numberString.startsWith('0')) numberString = numberString.slice(1); return { isNegative, normalizedNumberString: numberString, }; } /* eslint-disable complexity */ function getFirstNonZeroDigitToRightIndex(num) { for (let i = num.indexOf('.') + 1; i < num.length; i += 1) { if (num[i] !== '0') return i; } return undefined; } function sigfig(numberOrString, numSigfigs) { if (numSigfigs !== undefined && (numSigfigs <= 0 || !Number.isInteger(numSigfigs))) { throw new TypeError('The number of significant figures to round to must be an integer greater than 0.'); } const { normalizedNumberString: numberString, isNegative } = normalizeNumberString(convertNumberToString(numberOrString)); // By this point, a number will always be represented either .xyz or xyz. const decimalIndex = numberString.indexOf('.'); // Handle the case when there are only zeros in the number if (/^[0.]+$/.test(numberString)) { if (numSigfigs === undefined) { if (numberString === '.') return 1; else return numberString.length - 1; } else { if (numSigfigs === 1) { return '0'; } else { return `0.${'0'.repeat(numSigfigs)}`; } } } const firstNonZeroDigitToRightIndex = getFirstNonZeroDigitToRightIndex(numberString); if (numSigfigs === undefined) { if (numberString.startsWith('.')) { return numberString.length - firstNonZeroDigitToRightIndex; } else { return numberString.length - 1; } } else { const roundedDecimal = roundDecimal(numberString, numSigfigs); const wholeNumberLength = decimalIndex; let answer = roundedDecimal.padEnd(Math.max(wholeNumberLength, roundedDecimal.includes('.') ? numSigfigs + 1 : numSigfigs), '0'); // Normalizing the answer if (answer.startsWith('.')) { answer = '0' + answer; } if (answer.endsWith('.')) { answer = answer.slice(0, -1); } return isNegative ? `-${answer}` : answer; } } function roundDecimal(numberString, numSigfigs) { const digits = []; let roundingDigit; let firstNonZeroDigitIndex; let numSigfigsEncountered = 0; if (numberString.startsWith('.')) numberString = '0' + numberString; for (let i = 0; i < numberString.length; i += 1) { const digit = numberString[i]; digits.push(digit); if (firstNonZeroDigitIndex === undefined && digit !== '0' && digit !== '.') { firstNonZeroDigitIndex = i; } if (firstNonZeroDigitIndex !== undefined && digit >= '0' && digit <= '9') { numSigfigsEncountered += 1; if (numSigfigsEncountered === numSigfigs) { roundingDigit = (numberString[i + 1] === '.' ? numberString[i + 2] : numberString[i + 1]) ?? '0'; break; } } } if (firstNonZeroDigitIndex === undefined) { throw new Error('firstNonZeroDigitIndex should not be undefined'); } for (let i = numSigfigsEncountered; i < numSigfigs; i += 1) { digits.push('0'); } if (roundingDigit === undefined || roundingDigit < '5') { return digits.join(''); } let carry = false; for (let i = digits.length - 1; i >= 0; i -= 1) { const digit = digits[i]; if (digit === '.') { continue; } if (digit === '9') { digits[i] = 0; carry = true; } else { if (i < firstNonZeroDigitIndex) { digits.pop(); } digits[i] = String(Number(digit) + 1); carry = false; break; } } // The first digit is now a "0" if (carry) { digits.unshift('1'); } if (digits.at(-1) === '.') digits.pop(); return digits.join(''); } module.exports = sigfig;