Reputation: 208
I tried below sample code
function sigFigs(n, sig) {
if ( n === 0 )
return 0
var mult = Math.pow(10,
sig - Math.floor(Math.log(n < 0 ? -n: n) / Math.LN10) - 1);
return Math.round(n * mult) / mult;
}
But this function is not working for inputs like sigFigs(24730790,3) returns 24699999.999999996 and sigFigs(4.7152e-26,3) returns: 4.7200000000000004e-26
If anybody has working example please share. Thanks.
Upvotes: 17
Views: 28269
Reputation: 76
You can round a value to any arbitrary number of significant figures simply by first calculating the largest power of ten that the number contains, and then use that to calculate the precision to which the number should be rounded.
/**
* Rounds a value to a given number of significant figures
* @param value The number to round
* @param significanFigures The number of significant figures
* @returns The value rounded to the given number of significant figures
*/
const round = (value, significantFigures) => {
const exponent = Math.floor(Math.log10(value))
const nIntegers = exponent + 1
const precision = 10 ** (nIntegers - significantFigures)
return Math.round(value / precision) * precision
}
In the above code,
precision
simply means the closest multiple we want to round to. For example, if we want to round to the closest multiple of 100, the precision is 100. If we want to round to the closest multiple of 0.1, i.e. to one decimal place, the precision is 0.1.The
exponent
is simply the exponent of the largest power of 10 contained in value. Continue reading below, if you're interested in knowing where this comes from.
nIntegers
is the number of integers (digits to the left of the decimal place) in the value.
Some examples
> round(173.25, 1)
200
> round(173.25, 2)
170
> round(173.25, 3)
173
> round(173.25, 4)
173.3
> round(173.25, 5)
173.25
Generally, we can round any number to a given precision, by first "moving the decimal place" in one direction (division), then rounding the number to the nearest integer, and finally moving the decimal place back to its original position (multiplication).
const rounded = Math.round(value / precision) * precision
For example, to round a value to the closest multiple of 10, the precision is set to 10 and we get
> Math.round(173.25 / 10) * 10
170
Similarly, if we want to round a value to one decimal place, i.e. find the closest multiple of 0.1, the precision is set to 0.1
> Math.round(173.25 / 0.1) * 0.1
173.3
Here the
precision
simply means "the closest multiple we want to round to".
So how do we use this knowledge to round a value to any given number of significant figures then?
The problem we have to solve is to determine the precision we should round to, given the number of significant figures. Say we want to round the value 12345.67 to three significant figures. How do we determine that the precision should be 100, in this case?
The precision should be 100 because
Math.round(12345.67 / 100) * 100
gives 12300, i.e. rounded to three significant figures.
It's actually really easy to solve.
Basically, what we have to do is to 1) determine how many digits there are on the left side of the decimal place and then 2) use that to determine how many steps to "move the decimal place" before rounding the number.
We start by counting the number of digits in the integer part of the number (that is, 5 digits) and subtract the number of significant figures we want to round to (3 digits). The result, 5 - 3 = 2, is the number of steps we should move the decimal place to the left (if the result was negative we would move the decimal place to the right).
In order to move the decimal place two steps to the left, we have to use the precision 10^2 = 100. In other words, the result gives us the power to which 10 should be raised in order to get the precision.
n_integer = "number of digits in the integer part of the number" = 5
n_significant = "the number of significant figures we want to round" = 3
precision = 10 ** (n_integer - n_significant)
That's it!
But, hey, wait a minute! You haven't actually showed us how to code this! Also, you said we didn't want to use strings. How do we count the number of digits in the integer part of the number without converting it to a string? Well, we don't have to use strings for that. Math comes to the rescue!
We know that a real value v can be expressed as a power of ten (using ten because we're working in the decimal system). That is, v = 10^a. If we now only take the integer part of a, let's call that a', the new value v' = 10^a' will be the largest power of 10 contained in v. The number of digits in v is the same as in v', which is a' + 1. As such, we have shown that n_integer = a' + 1, where a' = floor(log10(v)).
In code this looks like
const exponent = Math.floor(Math.log10(v)) // a'
const nIntegers = exponent + 1
And, as we had from before, the precision is
const precision = 10 ** (nInteger - nSignificant)
And, finally, the rounding
return Math.round(value / precision) * precision
Example
Say we want to round the value v = 12345.67 to 1 significant figure. For the above code to work, the precision has to be precision = 10000 = 10^(n_integers - 1).If we wanted to round to 6 significant figures, the precision would have to be precision = 0.1 = 10^(n_integers - 6).
Generally, the precision has to be precision = 10^(n_integers - n_significant)
Using this code and knowledge, you can round a number to the closest multiple of any value, not just the plain old and boring powers of 10 (i.e. {..., 1000, 100, 10, 1, 0.1, 0.01, ...}). No, with this you can, for example, round to the closest multiple of, say, 0.3.
> Math.round(4 / 0.3) * 0.3
3.9
0.3 * 13 = 3.9, which is the multiple of 0.3 closest to 4.
Upvotes: 6
Reputation: 6002
if you want to specify significant figures left of the decimal place and replace extraneous placeholders with T B M K respectively
// example to 3 sigDigs (significant digits)
//54321 = 54.3M
//12300000 = 12.3M
const moneyFormat = (num, sigDigs) => {
var s = num.toString();
let nn = "";
for (let i = 0; i <= s.length; i++) {
if (s[i] !== undefined) {
if (i < sigDigs) nn += s[i];
else nn += "0";
}
}
nn = nn
.toString()
.replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
.replace(",000,000,000", "B")
.replace(",000,000", "M")
.replace(",000", "k");
if (
nn[nn.length - 4] === "," &&
nn[nn.length - 2] === "0" &&
nn[nn.length - 1] === "0"
) {
let numLetter = "K";
if (parseInt(num) > 999999999999) numLetter = "T";
else if (parseInt(num) > 999999999) numLetter = "B";
else if (parseInt(num) > 999999) numLetter = "M";
console.log("numLetter: " + numLetter);
nn = nn.toString();
let nn2 = ""; // new number 2
for (let i = 0; i < nn.length - 4; i++) {
nn2 += nn[i];
}
nn2 += "." + nn[nn.length - 3] + numLetter;
nn = nn2;
}
return nn;
};
Upvotes: 1
Reputation: 4421
How about automatic type casting, which takes care of exponential notation?
f = (x, n) => +x.toPrecision(n)
Testing:
> f (0.123456789, 6)
0.123457
> f (123456789, 6)
123457000
> f (-123456789, 6)
-123457000
> f (-0.123456789, 6)
-0.123457
> f (-0.123456789, 2)
-0.12
> f (123456789, 2)
120000000
And it returns a number and not a string.
Upvotes: 6
Reputation: 3753
First of all thanks to everybody, it would be a hard task without these snippets shared.
My value added, is the following snippet (see below for complete implementation)
parseFloat(number.toPrecision(precision))
Please note that if number is, for instance, 10000 and precision is 2, then number.toPrecision(precision)
will be '1.0e+4' but parseFloat
understands exponential notation.
It is also worth to say that, believe it or not, the algorithm using Math.pow
and logarithms posted above, when run on test case formatNumber(5, 123456789)
was giving a success on Mac (node v12) but rising and error on Windows (node v10). It was weird so we arrived at the solution above.
At the end I found this as the definitive implementation, taking advantage of all feedbacks provided in this post. Assuming we have a formatNumber.js file with the following content
/**
* Format number to significant digits.
*
* @param {Number} precision
* @param {Number} number
*
* @return {String} formattedValue
*/
export default function formatNumber (precision, number) {
if (typeof number === 'undefined' || number === null) return ''
if (number === 0) return '0'
const roundedValue = round(precision, number)
const floorValue = Math.floor(roundedValue)
const isInteger = Math.abs(floorValue - roundedValue) < Number.EPSILON
const numberOfFloorDigits = String(floorValue).length
const numberOfDigits = String(roundedValue).length
if (numberOfFloorDigits > precision) {
return String(floorValue)
} else {
const padding = isInteger ? precision - numberOfFloorDigits : precision - numberOfDigits + 1
if (padding > 0) {
if (isInteger) {
return `${String(floorValue)}.${'0'.repeat(padding)}`
} else {
return `${String(roundedValue)}${'0'.repeat(padding)}`
}
} else {
return String(roundedValue)
}
}
}
function round (precision, number) {
return parseFloat(number.toPrecision(precision))
}
If you use tape for tests, here there are some basic tests
import test from 'tape'
import formatNumber from '..path/to/formatNumber.js'
test('formatNumber', (t) => {
t.equal(formatNumber(4, undefined), '', 'undefined number returns an empty string')
t.equal(formatNumber(4, null), '', 'null number return an empty string')
t.equal(formatNumber(4, 0), '0')
t.equal(formatNumber(4, 1.23456789), '1.235')
t.equal(formatNumber(4, 1.23), '1.230')
t.equal(formatNumber(4, 123456789), '123500000')
t.equal(formatNumber(4, 1234567.890123), '1235000')
t.equal(formatNumber(4, 123.4567890123), '123.5')
t.equal(formatNumber(4, 12), '12.00')
t.equal(formatNumber(4, 1.2), '1.200')
t.equal(formatNumber(4, 1.234567890123), '1.235')
t.equal(formatNumber(4, 0.001234567890), '0.001235')
t.equal(formatNumber(5, 123456789), '123460000')
t.end()
})
Upvotes: 7
Reputation: 1168
Unfortunately the inbuilt method will give you silly results when the number is > 10, like exponent notation etc.
I made a function, which should solve the issue (maybe not the most elegant way of writing it but here it goes):
function(value, precision) {
if (value < 10) {
value = parseFloat(value).toPrecision(precision)
} else {
value = parseInt(value)
let significantValue = value
for (let i = value.toString().length; i > precision; i--) {
significantValue = Math.round(significantValue / 10)
}
for (let i = 0; significantValue.toString().length < value.toString().length; i++ ) {
significantValue = significantValue * 10
}
value = significantValue
}
return value
}
If you prefer having exponent notation for the higher numbers, feel free to use toPrecision() method.
Upvotes: 2