Is there a reliable way in JavaScript to obtain the number of decimal places of an arbitrary number?
Asked Answered
V

13

42

It's important to note that I'm not looking for a rounding function. I am looking for a function that returns the number of decimal places in an arbitrary number's simplified decimal representation. That is, we have the following:

decimalPlaces(5555.0);     //=> 0
decimalPlaces(5555);       //=> 0
decimalPlaces(555.5);      //=> 1
decimalPlaces(555.50);     //=> 1
decimalPlaces(0.0000005);  //=> 7
decimalPlaces(5e-7);       //=> 7
decimalPlaces(0.00000055); //=> 8
decimalPlaces(5.5e-7);     //=> 8

My first instinct was to use the string representations: split on '.', then on 'e-', and do the math, like so (the example is verbose):

function decimalPlaces(number) {
  var parts = number.toString().split('.', 2),
    integerPart = parts[0],
    decimalPart = parts[1],
    exponentPart;

  if (integerPart.charAt(0) === '-') {
    integerPart = integerPart.substring(1);
  }

  if (decimalPart !== undefined) {
    parts = decimalPart.split('e-', 2);
    decimalPart = parts[0];
  }
  else {
    parts = integerPart.split('e-', 2);
    integerPart = parts[0];
  }
  exponentPart = parts[1];

  if (exponentPart !== undefined) {
    return integerPart.length +
      (decimalPart !== undefined ? decimalPart.length : 0) - 1 +
      parseInt(exponentPart);
  }
  else {
    return decimalPart !== undefined ? decimalPart.length : 0;
  }
}

For my examples above, this function works. However, I'm not satisfied until I've tested every possible value, so I busted out Number.MIN_VALUE.

Number.MIN_VALUE;                      //=> 5e-324
decimalPlaces(Number.MIN_VALUE);       //=> 324

Number.MIN_VALUE * 100;                //=> 4.94e-322
decimalPlaces(Number.MIN_VALUE * 100); //=> 324

This looked reasonable at first, but then on a double take I realized that 5e-324 * 10 should be 5e-323! And then it hit me: I'm dealing with the effects of quantization of very small numbers. Not only are numbers being quantized before storage; additionally, some numbers stored in binary have unreasonably long decimal representations, so their decimal representations are being truncated. This is unfortunate for me, because it means that I can't get at their true decimal precision using their string representations.

So I come to you, StackOverflow community. Does anyone among you know a reliable way to get at a number's true post-decimal-point precision?

The purpose of this function, should anyone ask, is for use in another function that converts a float into a simplified fraction (that is, it returns the relatively coprime integer numerator and nonzero natural denominator). The only missing piece in this outer function is a reliable way to determine the number of decimal places in the float so I can multiply it by the appropriate power of 10. Hopefully I'm overthinking it.

Vindictive answered 2/3, 2012 at 19:53 Comment(6)
There are an infinite number of decimal places in any floating-point number.Orthogenesis
You may be interested in this: blog.thatscaptaintoyou.com/…Bellis
@LightnessRacesinOrbit: right, well I couldn't include all the details in the title! I do specify in the question, however, that I'm looking for the simplified representation.Vindictive
@GGG: Wicked, he mentions fractional numbers in the description. I might either use this for inspiration or use this. Thanks!Vindictive
@Vindictive no problem, there is also at least one other big numbers js library floating around somewhere.Bellis
@Milosz: the accepted answer fails for 0.123. I wrote an answer that works for all the corner cases I could image, and is very simple as well.Missend
I
26

Historical note: the comment thread below may refer to first and second implementations. I swapped the order in September 2017 since leading with a buggy implementation caused confusion.

If you want something that maps "0.1e-100" to 101, then you can try something like

function decimalPlaces(n) {
  // Make sure it is a number and use the builtin number -> string.
  var s = "" + (+n);
  // Pull out the fraction and the exponent.
  var match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(s);
  // NaN or Infinity or integer.
  // We arbitrarily decide that Infinity is integral.
  if (!match) { return 0; }
  // Count the number of digits in the fraction and subtract the
  // exponent to simulate moving the decimal point left by exponent places.
  // 1.234e+2 has 1 fraction digit and '234'.length -  2 == 1
  // 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5
  return Math.max(
      0,  // lower limit.
      (match[1] == '0' ? 0 : (match[1] || '').length)  // fraction length
      - (match[2] || 0));  // exponent
}

According to the spec, any solution based on the builtin number->string conversion can only be accurate to 21 places beyond the exponent.

9.8.1 ToString Applied to the Number Type

  1. Otherwise, let n, k, and s be integers such that k ≥ 1, 10k−1 ≤ s < 10k, the Number value for s × 10n−k is m, and k is as small as possible. Note that k is the number of digits in the decimal representation of s, that s is not divisible by 10, and that the least significant digit of s is not necessarily uniquely determined by these criteria.
  2. If k ≤ n ≤ 21, return the String consisting of the k digits of the decimal representation of s (in order, with no leading zeroes), followed by n−k occurrences of the character ‘0’.
  3. If 0 < n ≤ 21, return the String consisting of the most significant n digits of the decimal representation of s, followed by a decimal point ‘.’, followed by the remaining k−n digits of the decimal representation of s.
  4. If −6 < n ≤ 0, return the String consisting of the character ‘0’, followed by a decimal point ‘.’, followed by −n occurrences of the character ‘0’, followed by the k digits of the decimal representation of s.

Historical note: The implementation below is problematic. I leave it here as context for the comment thread.

Based on the definition of Number.prototype.toFixed, it seems like the following should work but due to the IEEE-754 representation of double values, certain numbers will produce false results. For example, decimalPlaces(0.123) will return 20.

function decimalPlaces(number) {
  // toFixed produces a fixed representation accurate to 20 decimal places
  // without an exponent.
  // The ^-?\d*\. strips off any sign, integer portion, and decimal point
  // leaving only the decimal fraction.
  // The 0+$ strips off any trailing zeroes.
  return ((+number).toFixed(20)).replace(/^-?\d*\.?|0+$/g, '').length;
}

// The OP's examples:
console.log(decimalPlaces(5555.0));  // 0
console.log(decimalPlaces(5555));  // 0
console.log(decimalPlaces(555.5));  // 1
console.log(decimalPlaces(555.50));  // 1
console.log(decimalPlaces(0.0000005));  // 7
console.log(decimalPlaces(5e-7));  // 7
console.log(decimalPlaces(0.00000055));  // 8
console.log(decimalPlaces(5e-8));  // 8
console.log(decimalPlaces(0.123));  // 20 (!)
Irriguous answered 2/3, 2012 at 20:13 Comment(15)
Very informative, thank you. I think I might end up using toFixed(20) for simplicity's sake and ignore the really really small numbers. The quantization line has to be drawn somewhere, and 20 isn't low enough to be annoying for most purposes.Vindictive
@Milosz, Yeah. If I was writing a library, I would go with the second approach, but for specific applications, the first may be sufficient. One thing to note though is that the first doesn't deal as well with NaN and ±Infinity.Irriguous
This isn't working for me in Google Chrome version 30. Input of 555.50 results in an output of 1, but 555.20 results in an output of 20.Eakins
One would think this would be easier. On the other hand there is no problem in the world that a big enough regular expression can not solve. +1 for using MDN instead of w3schoolsParish
I did some testing ((+number).toFixed(20)) with number= 0.2 is returning "0.20000000000000001110" which screws up your method. I tested on chrome.Parish
0.2 is not representable exactly in binary, and the decimal value that is closest to the IEEE-754 double value that is closest to 0.2 does have that many decimal places. I can only reason about the double values that reach my code, so I think the disconnect here is in the loss of precision due to the translation from a semantically meaningful "0.2" to an IEEE-754 value.Irriguous
unfortunately, this loss of precision makes the toFixed(20) solution pretty unreliable, since I'm getting a fairly consistent 20 for numbers like 0.123, 0.121, 0.1215 etc. Although I've only tested in ChromeWedding
@SuckerForMayhem, try the second one. I get 3,3,4 for those inputs.Irriguous
I get 19 for 1.31606 due to how V8 represents the numbers. Tested in Chrome and Node. This is just not a reliable solution.Missend
what about the languages that separate the decimal part with ',' ?Ramrod
@DanDascalescu, try the second one. decimalPlaces('1.31606') === 5Irriguous
@DanDascalescu, I just reordered things to avoid confusion. The second implementation is now first.Irriguous
@khodayarJ, In human locales, commas are used as decimal separators in many locales and digit group separators in others. This function is meant to operate on numbers formatted per JavaScript rules for parsing by machines.Irriguous
This function isn't type complaint as match[2] is a string which is converted into a number without parseInt()Lilytrotter
@daniol, What problems do you foresee? match[2] is the right operand of - which coerces its operands to numbers. ES262 § 7.1.3.1 mentions OctalLiteral but in my testing 0 - "012" === 12 parses as decimal.Irriguous
B
12

Well, I use a solution based on the fact that if you multiply a floating-point number by the right power of 10, you get an integer.

For instance, if you multiply 3.14 * 10 ^ 2, you get 314 (an integer). The exponent represents then the number of decimals the floating-point number has.

So, I thought that if I gradually multiply a floating-point by increasing powers of 10, you eventually arrive to the solution.

let decimalPlaces = function () {
   function isInt(n) {
      return typeof n === 'number' && 
             parseFloat(n) == parseInt(n, 10) && !isNaN(n);
   }
   return function (n) {
      const a = Math.abs(n);
      let c = a, count = 1;
      while (!isInt(c) && isFinite(c)) {
         c = a * Math.pow(10, count++);
      }
      return count - 1;
   };
}();

for (const x of [
  0.0028, 0.0029, 0.0408,
  0, 1.0, 1.00, 0.123, 1e-3,
  3.14, 2.e-3, 2.e-14, -3.14e-21,
  5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
  0.000006, 0.0000007,
  0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));
Bullough answered 2/12, 2013 at 17:56 Comment(5)
+1 for not using reqular expressions and for a solution that is not limited to any specific number of decimals. Only problem I see is numbers with an infinite number of decimals like 1/3.Stumer
@DaniëlTulp Even numbers with infinite decimal notation must represented with a finite number of decimals in computer memory. I guess this approach would tell you how many decimals that finite notation has. I suppose that to accurately represented 1/3 we would have to use fractions, since the decimal notation wouldn't work.Bullough
this has been the most consistent approach for me. I feel more comfortable using math then converting to strings, props!Wedding
It fails for 0.0029, returning 5 instead of 4. Floating point in JavaScript is a minefield.Missend
Likewise, it returns 6 for 0.0408 instead of 4. Seems like there isn't really a straight-forward solution without any gotchas.Arterialize
M
5

2017 Update

Here's a simplified version based on Edwin's answer. It has a test suite and returns the correct number of decimals for corner cases including NaN, Infinity, exponent notations, and numbers with problematic representations of their successive fractions, such as 0.0029 or 0.0408. This covers the vast majority of financial applications, where 0.0408 having 4 decimals (not 6) is more important than 3.14e-21 having 23.

function decimalPlaces(n) {
  function hasFraction(n) {
    return Math.abs(Math.round(n) - n) > 1e-10;
  }

  let count = 0;
  // multiply by increasing powers of 10 until the fractional part is ~ 0
  while (hasFraction(n * (10 ** count)) && isFinite(10 ** count))
    count++;
  return count;
}

for (const x of [
  0.0028, 0.0029, 0.0408, 0.1584, 4.3573, // corner cases against Edwin's answer
  11.6894,
  0, 1.0, 1.00, 0.123, 1e-3, -1e2, -1e-2, -0.1,
  NaN, 1E500, Infinity, Math.PI, 1/3,
  3.14, 2.e-3, 2.e-14,
  1e-9,  // 9
  1e-10,  // should be 10, but is below the precision limit
  -3.14e-13,  // 15
  3.e-13,  // 13
  3.e-14,  // should be 14, but is below the precision limit
  123.12345678901234567890,  // 14, the precision limit
  5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
  0.000006, 0.0000007,
  0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));

The tradeoff is that the method is limited to maximum 10 guaranteed decimals. It may return more decimals correctly, but don't rely on that. Numbers smaller than 1e-10 may be considered zero, and the function will return 0. That particular value was chosen to solve correctly the 11.6894 corner case, for which the simple method of multiplying by powers of 10 fails (it returns 5 instead of 4).

However, this is the 5th corner case I've discovered, after 0.0029, 0.0408, 0.1584 and 4.3573. After each, I had to reduce the precision by one decimal. I don't know if there are other numbers with less than 10 decimals for which this function may return an incorrect number of decimals. To be on the safe side, look for an arbitrary precision library.

Note that converting to string and splitting by . is only a solution for up to 7 decimals. String(0.0000007) === "7e-7". Or maybe even less? Floating point representation isn't intuitive.

Missend answered 29/6, 2017 at 3:14 Comment(1)
+1 Although this function has known limitations, it works for what I need it (any reasonable amount of decimals for human consumption, namely 0 - 4 in my app)Arterialize
N
5

Simple "One-Liner":

If what you're doing requires more than 16 digit precision, then this is not for you.

This 'one-liner' will work fine for the other 99.99999999999999% of the time. (Yes, even that number.)😜

function numDec(n){return n%1==0?0:(""+n).length-(""+n).lastIndexOf(".")-1}

Demo in the snippet:

function numDec(n){return n%1==0?0:(""+n).length-(""+n).lastIndexOf(".")-1}


setInterval(function(){
  n=Math.random()*10000000000;
  document.body.innerHTML=n+' ← '+numDec(n)+' decimal places';
},777);
body{font-size:123.4567890%; font-family:'fira code';}

More info:

Nunez answered 28/9, 2021 at 4:10 Comment(0)
E
3

this works for numbers smaller than e-17 :

function decimalPlaces(n){
    var a;
    return (a=(n.toString().charAt(0)=='-'?n-1:n+1).toString().replace(/^-?[0-9]+\.?([0-9]+)$/,'$1').length)>=1?a:0;
}
Extractor answered 2/3, 2012 at 20:8 Comment(3)
This reminds me that I forgot to include checking for minus signs at the beginning of the number. Thanks! I also enjoy how concise this function is. Pushing the number past 1 in absolute value to avoid the negative exponent is clever (though I don't understand why JavaScript switches to exponent notation so much later for positive exponents). If nothing that works past e-17 comes along, this is probably what I'll go with.Vindictive
As of the last edit (April 1, 2013) on this answer it return (correctly) 1 for 1.2. It howerver returns 1 (instead of zero) for numbers with no decimal placesParish
My answer fixes this problem I found. I'm not sure why you are checking the chartAt(0) for the minus sign since you can just ignore it on the regex analyzing only the part after the '.' character.Parish
S
3

This works for me

const decimalPlaces = value.substring(value.indexOf('.') + 1).length;

This method expects the value to be a standard number.

Surfboat answered 19/7, 2019 at 1:54 Comment(0)
B
1

Not only are numbers being quantized before storage; additionally, some numbers stored in binary have unreasonably long decimal representations, so their decimal representations are being truncated.

JavaScript represents numbers using IEEE-754 double-precision (64 bit) format. As I understand it this gives you 53 bits precision, or fifteen to sixteen decimal digits.

So for any number with more digits you just get an approximation. There are some libraries around to handle large numbers with more precision, including those mentioned in this thread.

Bamford answered 2/3, 2012 at 20:29 Comment(0)
S
1

2021 Update

An optimized version of Mike Samuel handling scientific and non-scientific representation.

// Helper function to extract the number of decimal assuming the 
// input is a number (either as a number of a stringified number)
// Note: if a stringified number has an exponent, it will always be
// '<x>e+123' or '<x>e-123' or '<x.dd...d>e+123' or '<x.dd...d>e-123'.
// No need to check for '<x>e123', '<x>E+123', '<x>E-123' etc.
const _numDecimals = v => {
  const [i, p, d, e, n] = v.toString().split(/(\.|e[\-+])/g);
  const f = e === 'e-';
  return ((p === '.' && (!e || f) && d.length) + (f && parseInt(n)))
    || (p === 'e-' && parseInt(d))
    || 0;
}

// But if you want to be extra safe...you can replace _numDecimals
// with this:
const _numSafeDecimals = v => {
  let [i, p, d, e, n] = v.toString().split(/(\.|[eE][\-+])/g);
  e = e.toLowerCase();
  const f = e === 'e-';
  return ((p === '.' && (!e || f) && d.length) + (f && parseInt(n)))
    || (p.toLowerCase() === 'e-' && parseInt(d))
    || 0;
}

// Augmenting Number proto.
Number.prototype.numDecimals = function () {
  return (this % 1 !== 0 && _numDecimals(this)) || 0;
}

// Independent function.
const numDecimals = num => (
  (!isNaN(num) && num % 1 !== 0 && _numDecimals(num)) || 0
);

// Tests:
const test = n => (
  console.log('Number of decimals of', n, '=', n.numDecimals())
);
test(1.234e+2); // --> 1
test(0.123); // ---> 3
test(123.123); // ---> 3
test(0.000123); // ---> 6
test(1e-20); // --> 20
test(1.2e-20); // --> 21
test(1.23E-20); // --> 22
test(1.23456789E-20); // --> 28
test(10); // --> 0
test(1.2e20); // --> 0
test(1.2e+20); // --> 0
test(1.2E100); // --> 0
test(Infinity); // --> 0
test(-1.234e+2); // --> 1
test(-0.123); // ---> 3
test(-123.123); // ---> 3
test(-0.000123); // ---> 6
test(-1e-20); // --> 20
test(-1.2e-20); // --> 21
test(-1.23E-20); // --> 22
test(-1.23456789E-20); // --> 28
test(-10); // --> 0
test(-1.2e20); // --> 0
test(-1.2e+20); // --> 0
test(-1.2E100); // --> 0
test(-Infinity); // --> 0
Sackville answered 23/5, 2021 at 0:28 Comment(0)
P
1

An optimized version of nick answer.

The function requires that n is a string. This function gets the decimal even if there all 0, like 1.00 -> 2 decimals.

function getDecimalPlaces(n) {
    var i = n.indexOf($DecimalSeparator)
    return i > 0 ? n.length - i - 1 : 0
}

console.log(getDecimalPlaces("5555.0"));  // 1
console.log(getDecimalPlaces("5555"));  // 0
console.log(getDecimalPlaces("555.5"));  // 1
console.log(getDecimalPlaces("555.50"));  // 2
console.log(getDecimalPlaces("0.0000005"));  // 7
console.log(getDecimalPlaces("0.00000055"));  // 8
console.log(getDecimalPlaces("0.00005500"));  // 8
Pluckless answered 7/7, 2021 at 11:55 Comment(0)
I
0

If you have very small values, use the below code:

Number.prototype.countDecimals = function () {

    if (Math.floor(this.valueOf()) === this.valueOf()) return 0;

    var str = this.toString();
    if (str.indexOf(".") !== -1 && str.indexOf("-") !== -1) {
      return parseInt(str.split("-")[1])+str.split("-")[0].split(".")[1].length-1
    } else if (str.indexOf(".") !== -1) {
        return str.split(".")[1].length || 0;
    }
    return str.split("-")[1] || 0;
}


var num = 10
console.log(num.countDecimals()) //0

num = 1.23
console.log(num.countDecimals()) //2

num = 1.454689451
console.log(num.countDecimals()) //9

num = 1.234212344244323e-7
console.log(num.countDecimals()) //22
Immaculate answered 13/4, 2022 at 17:3 Comment(0)
N
0

One more version to handle all numbers within JavaScript precision limit

function decimalPlaces(n) {
    return 0 < n%1 ? (n=(""+n).split(/[.e]/))[1] < 0 ? -n[1] : n[1].length-(0|n[2]) : 0
}
decimalPlaces(4.3573)                           // 4
decimalPlaces(0.000000000000000000000000000123) // 30
Nepheline answered 23/7, 2023 at 12:0 Comment(0)
H
0
const decimalPlaces = v => v?.toString().split('.')[1]?.length || 0;

One liner, depends on Optional chaining (?.) available in modern browsers. Same (integer length) limitations as other answers.

Halfassed answered 18/3 at 9:41 Comment(0)
P
-1

Based on gion_13 answer I came up with this:

function decimalPlaces(n){
let result= /^-?[0-9]+\.([0-9]+)$/.exec(n);
return result === null ? 0 : result[1].length;
}

for (const x of [
  0, 1.0, 1.00, 0.123, 1e-3, 3.14, 2.e-3, -3.14e-21,
  5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
  0.000006, 0.0000007,
  0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));

It fixes the returning 1 when there are no decimal places. As far as I can tell this works without errors.

Parish answered 26/12, 2013 at 15:45 Comment(1)
It works without errors only for the very few test cases you've included :)Missend

© 2022 - 2024 — McMap. All rights reserved.