24

I was interested to have the following getNumberOfDecimalPlace function:

System.out.println("0 = " + Utils.getNumberOfDecimalPlace(0));          // 0
System.out.println("1.0 = " + Utils.getNumberOfDecimalPlace(1.0));      // 0
System.out.println("1.01 = " + Utils.getNumberOfDecimalPlace(1.01));    // 2
System.out.println("1.012 = " + Utils.getNumberOfDecimalPlace(1.012));  // 3
System.out.println("0.01 = " + Utils.getNumberOfDecimalPlace(0.01));    // 2
System.out.println("0.012 = " + Utils.getNumberOfDecimalPlace(0.012));  // 3

May I know how can I implement getNumberOfDecimalPlace, by using BigDecimal?

The following code doesn't work as expected:

public static int getNumberOfDecimalPlace(double value) {
    final BigDecimal bigDecimal = new BigDecimal("" + value);
    final String s = bigDecimal.toPlainString();
    System.out.println(s);
    final int index = s.indexOf('.');
    if (index < 0) {
        return 0;
    }
    return s.length() - 1 - index;
}

The following get printed :

0.0
0 = 1
1.0
1.0 = 1
1.01
1.01 = 2
1.012
1.012 = 3
0.01
0.01 = 2
0.012
0.012 = 3

However, for case 0, 1.0, it doesn't work well. I expect, "0" as result. But they turned out to be "0.0" and "1.0". This will return "1" as result.

msrd0
  • 7,816
  • 9
  • 47
  • 82
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875

12 Answers12

57

Combining Turismo, Robert and user1777653's answers, we've got:

int getNumberOfDecimalPlaces(BigDecimal bigDecimal) {
    return Math.max(0, bigDecimal.stripTrailingZeros().scale());
}
  • stripTrailingZeros() ensures that trailing zeros are not counted (e.g. 1.0 has 0 decimal places).
  • scale() is more efficient than String.indexOf().
  • A negative scale() represents zero decimal places.

There you have it, the best of both worlds.

Community
  • 1
  • 1
Gili
  • 86,244
  • 97
  • 390
  • 689
  • This might be wrong, because `10` becomes `1E1` after `stripTrailingZeros()`, and then the `scale()` is `-1`, not `0`. – Eric Feb 09 '18 at 04:18
  • @EricWang The code uses `Math.max(0, ...)` so you will end up with `0` as expected. – Gili Feb 09 '18 at 04:46
  • Ok, didn't notice that . – Eric Feb 09 '18 at 09:47
  • 2
    This will fail when you construct BigDecimal using double value. e.g. `new BigDecimal(123.123).stripTrailingZeros().scale()` will return `46`. – Nandkumar Tekale Feb 28 '20 at 12:26
  • 1
    @NandkumarTekale You shouldn't be using the `double` constructor. See https://stackoverflow.com/q/12218515/14731 – Gili Mar 03 '20 at 14:46
  • if it's a `Double`, I would suggest you convert it to `String` then pass it to the `BigDecimal` constructor. i.e. `Math.max(0, BigDecimal(someDouble.toString()).stripTrailingZeros().scale())`. – billydh Jun 11 '20 at 23:50
  • @billydh Why? Would this add any precision? I can't see how it could. – Gili Jun 12 '20 at 04:06
  • Not for precision. it is to ensure the BigDecimal value is the same as the original Double value, e.g. `BigDecimal(123.123).stripTrailingZeros().scale()` returns 46 because `BigDecimal(123.123)` results in 123.12332127461876481476238467283 (just random number but you know what i mean). If we cast the Double value to String before passing to BigDecimal constructor, then it will result in the same value, e.g. 123.123. – billydh Jun 12 '20 at 04:34
  • @billydh It's not clear how you plan to cast a `double` to a `String` but if (for example) you use [Double.toString(double)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Double.html#toString(double)) then you will end up with the rounding rules mentioned in the Javadoc. There is no magic. You will end up with as little or as much precision as your binary representation had to begin with. – Gili Oct 23 '21 at 20:23
36

This code:

int getNumberOfDecimalPlaces(BigDecimal bigDecimal) {
    String string = bigDecimal.stripTrailingZeros().toPlainString();
    int index = string.indexOf(".");
    return index < 0 ? 0 : string.length() - index - 1;
}

... passes these tests:

assertThat(getNumberOfDecimalPlaces(new BigDecimal("0.001")), equalTo(3));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("0.01")), equalTo(2));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("0.1")), equalTo(1));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("1.000")), equalTo(0));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("1.00")), equalTo(0));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("1.0")), equalTo(0));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("1")), equalTo(0));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("10")), equalTo(0));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("10.1")), equalTo(1));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("10.01")), equalTo(2));
assertThat(getNumberOfDecimalPlaces(new BigDecimal("10.001")), equalTo(3));

... if that is indeed what you want. The other replies are correct, you have to use BigDecimal all the way through for this rather than double/float.

Robert Atkins
  • 23,528
  • 15
  • 68
  • 97
7

Without having to convert to String, it should be more efficient to use the scale directly:

  private int getNumberOfDecimalPlaces(BigDecimal bigDecimal)
  {
    int scale =  bigDecimal.stripTrailingZeros().scale();
    return scale>0?scale:0;
  }
Axel Podehl
  • 4,034
  • 29
  • 41
5

That should do it

int getNumberOfDecimalPlace(BigDecimal number) {
    int scale = number.stripTrailingZeros().scale();
    return scale > 0 ? scale : 0;
}
user1777653
  • 121
  • 1
  • 2
4

If you really get doubles i recommend formating them first as strings before creating the BigDecimal. At least that has worked for me: How to check if a double has at most n decimal places?

Depending on how many digits you expect you can either use standard formating like

String.valueOf(doubleValue);

or you could use specialised formatting to avoid exponential format

DecimalFormat decimalFormat = new DecimalFormat();
decimalFormat.setMaximumIntegerDigits(Integer.MAX_VALUE);
// don't use grouping for numeric-type cells
decimalFormat.setGroupingUsed(false);
decimalFormat.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
value = decimalFormat.format(numericValue);

When you have a BigDecimal you can simply call scale() to get the number of decimal places.

Community
  • 1
  • 1
Turismo
  • 2,064
  • 2
  • 16
  • 23
  • 2
    The problem with scale() is that it fails to distinguish between decimal places and significant decimal places. It returns "1" for "1.0" when it should return 0, per the OP's request. – james.garriss Aug 27 '13 at 23:50
3

Try this:

Math.floor(Math.log(x) / Math.log(10))
0.001 = -3
0.01  = -2
0.1   = -1  
1     = 0  
10    = 1  
100   = 2
Lawrence Phillips
  • 289
  • 1
  • 3
  • 12
  • This is helpful for getting the position of the first significant number in the value. NB: But for values like 1.124 and I want to get the number of decimal places, it does not work. – JackDev Dec 13 '12 at 03:54
3

It's not your code that's wrong, but your expectations. double is based on a binary floating point representation and completely unfit for accurately representing decimal fractions. Decimal 0.1 e.g. has an infinite number of digits when represented in binary, thus it gets truncated and when converted back to decimal, you get erros in the least significant digits.

If you use BigDecimal exclusively, your code will work as expected.

Michael Borgwardt
  • 342,105
  • 78
  • 482
  • 720
  • "If you use BigDecimal exclusively, your code will work as expected." --> ? But it doesn't work for my case. – Cheok Yan Cheng Feb 19 '10 at 12:48
  • @Yan: you're not using BigDecimal exclusively. You must not use any `double` or `float` values **including** any literals of those types. – Joachim Sauer Feb 19 '10 at 12:52
  • 3
    @Yan: It does not work because you use double as your input parameter type. You have to avoid the use of double completely, because the second you put your decimal value into a double (even as an intermediate result), you have already corrupted it. – Michael Borgwardt Feb 19 '10 at 12:52
  • The problem with this answer is that even if the OP's code is converted to only use BigDecimals, it still won't work. Why not? Because it does not distinguish between decimal places and significant decimal places. – james.garriss Aug 27 '13 at 23:54
0

How about having a look at the javadoc of BigDecimal. I'm not sure, but I'd give getScale and getPercision a try.

er4z0r
  • 4,711
  • 8
  • 42
  • 62
0

The best way to get a BigDecimal with a specified number of decimal places is by using the setscale method over it. Personally I like to also use the rounding version of the method (see the link below): http://java.sun.com/j2se/1.5.0/docs/api/java/math/BigDecimal.html#setScale(int,%20int)

If you're wanting to get the number of decimal positions that a BigDecimal is currently set at call the associated scale() method.

SOA Nerd
  • 943
  • 5
  • 12
  • I am not interested in get "a BigDecimal with a specified number of decimal places". I am interested to know "a double with HOW MANY number of decimal places" – Cheok Yan Cheng Feb 19 '10 at 12:15
  • Ah....that's more of a problem. The issue is that a double is imprecise. For example, you may set the value of a double to 1.25 but when you look at it later the value returns as 1.24999999. This is because of the way that Java stores it. This kinda thing is why I avoid doubles as much as possible. – SOA Nerd Feb 19 '10 at 12:19
  • the description is correct, the example is not so good. 1.25 *can* be represented exactly in a `double`. 1.2 for example *can't*. – Joachim Sauer Feb 19 '10 at 12:47
  • @Joachim Yep...sorry. I must be low on coffee today :-). – SOA Nerd Feb 19 '10 at 12:53
0

Best option I have found so far (not needing toString + index):

public static int digitsStripTrailingZero(BigDecimal value)
{
    return digits(value.stripTrailingZeros());
}

public static int digits(BigDecimal value)
{
    return Math.max(0, value.scale());
}
-1

Michael Borgwardt answer is the correct one. As soon as you use any double or float, your values are already corrupted.

To provide a code example:

System.out.println("0 = " + BigDecimalUtil.getNumberOfDecimalPlace("0")); // 0
System.out.println("1.0 = " + BigDecimalUtil.getNumberOfDecimalPlace("1.0")); // 0
System.out.println("1.01 = " + BigDecimalUtil.getNumberOfDecimalPlace(new BigDecimal("1.01"))); // 2
System.out.println("1.012 = " + BigDecimalUtil.getNumberOfDecimalPlace(new BigDecimal("1.012"))); // 3
System.out.println("0.01 = " + BigDecimalUtil.getNumberOfDecimalPlace("0.01")); // 2
System.out.println("0.012 = " + BigDecimalUtil.getNumberOfDecimalPlace("0.012")); // 3
System.out.println("0.00000000000000000012 = " + BigDecimalUtil.getNumberOfDecimalPlace("0.00000000000000000012")); // 20

And an overloaded version of getNumberOfDecimalPlace so you could use it with BigDecimal or String:

public static int getNumberOfDecimalPlace(String value) {
    final int index = value.indexOf('.');
    if (index < 0) {
        return 0;
    }
    return value.length() - 1 - index;
}

public static int getNumberOfDecimalPlace(BigDecimal value) {
    return getNumberOfDecimalPlace(value.toPlainString());
}
Nils Schmidt
  • 3,702
  • 6
  • 23
  • 28
  • You sure BigDecimalUtil.getNumberOfDecimalPlace("1.0")) going to return 0? – Cheok Yan Cheng Feb 19 '10 at 14:00
  • The problem with this answer is that it fails to distinguish between decimal places and significant decimal places. As @Cheok rightly pointed out, it returns "1" for "1.0" when it should return 0, per the OP's request. – james.garriss Aug 27 '13 at 23:45
-7

Why not just change your code to get a doubles decimal places?

public static int getNumberOfDecimalPlace(double value) {
    //For whole numbers like 0
    if (Math.round(value) == value) return 0;
    final String s = Double.toString(value);
    System.out.println(s);
    final int index = s.indexOf('.');
    if (index < 0) {
       return 0;
    }
    return s.length() - 1 - index;
}
Clinton
  • 2,787
  • 1
  • 18
  • 10
  • 3
    @Valentin: Because the use of double is the fundamental problem. This is a step into exactly the wrong direction. – Michael Borgwardt Feb 19 '10 at 12:49
  • It does not work for double with large number of decimal places, for example 0.0000000000000000000000000000001. Double.toString(double) will return 1.0E-31. Your whole logic fails – Adisesha Feb 19 '10 at 12:49
  • okay, it didn't see it that way at first (I'm not the answerer, I was just wondering :)) – Valentin Rocher Feb 19 '10 at 12:54
  • @Michael ok, fine. No need for a downvote though as it was just a sugguestion. He seems to be passing doubles to it. And based on other comments it seemed like he just wanted the decimal places of the double value. – Clinton Feb 19 '10 at 12:57
  • I accept your answer because you are the only who follow my requirement. I make an amendment, by using DecimalFormat decimalFormat = new DecimalFormat("0.########"); final String s = decimalFormat.format(value); – Cheok Yan Cheng Feb 19 '10 at 14:22