sigfig
Version:
Round a number to n significant figures.
120 lines (119 loc) • 4.09 kB
JavaScript
/* eslint-disable complexity */
import { convertNumberToString, normalizeNumberString, } from '../utils/number.js';
function getFirstNonZeroDigitToRightIndex(num) {
for (let i = num.indexOf('.') + 1; i < num.length; i += 1) {
if (num[i] !== '0')
return i;
}
return undefined;
}
export 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('');
}