sigfig
Version:
Round a number to n significant figures.
240 lines (230 loc) • 8.37 kB
JavaScript
;
/**
* 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;