suresh inakollu
suresh inakollu

Reputation: 208

How can I round to an arbitrary number of significant digits with JavaScript?

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

Answers (6)

supercluster
supercluster

Reputation: 76

Using pure math (i.e. no strings involved)

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

An intuitive and educative explanation

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)

A fun side effect

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

Michael Nelles
Michael Nelles

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

punund
punund

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

Gianluca Casati
Gianluca Casati

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

Ahti Ahde
Ahti Ahde

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

D Mishra
D Mishra

Reputation: 1578

You can try javascript inbuilt method-

Number( my_number.toPrecision(3) )

For Your case try

Number( 24730790.0.toPrecision(5) )

For your refrence and working example you can see link

Upvotes: 33

Related Questions