views:

5823

answers:

6

Currently i have this method:

static boolean checkDecimalPlaces(double d, int decimalPlaces){
    if (d==0) return true;

    double multiplier = Math.pow(10, decimalPlaces); 
    double check  =  d * multiplier;
    check = Math.round(check);   
    check = check/multiplier; 
    return (d==check);   
}

But this method fails for checkDecmialPlaces(649632196443.4279, 4) probably because I do base 10 math on a base 2 number.

So how can this check be done correctly?

I thought of getting a string representation of the double value and then check that with a regexp - but that felt weird.

EDIT: Thanks for all the answers. There are cases where I really get a double and for those cases I implemented the following:

private static boolean checkDecimalPlaces(double d, int decimalPlaces) {
 if (d == 0) return true;

 final double epsilon = Math.pow(10.0, ((decimalPlaces + 1) * -1));

 double multiplier = Math.pow(10, decimalPlaces);
 double check = d * multiplier;
 long checkLong = (long) Math.abs(check);
 check = checkLong / multiplier;

 double e = Math.abs(d - check);
 return e < epsilon;
}

I changed the round to a truncation. Seems that the computation done in round increases the inaccuracy too much. At least in the failing testcase.
As some of you pointed out if I could get to the 'real' string input I should use BigDecimal to check and so I have done:

BigDecimal decimal = new BigDecimal(value);
BigDecimal checkDecimal = decimal.movePointRight(decimalPlaces);
return checkDecimal.scale() == 0;

The double value I get comes from the Apache POI API that reads excel files. I did a few tests and found out that although the API returns double values for numeric cells I can get a accurate representation when I immediately format that double with the DecimalFormat:

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);

This also works for values that can't be represented exactly in binary format.

+3  A: 

As with all floating point arithmetic, you should not check for equality, but rather that the error (epsilon) is sufficiently small.

If you replace:

return (d==check);

with something like

return (Math.abs(d-check) <= 0.0000001);

it should work. Obviously, the epsilon should be selected to be small enough compared with the number of decimals you're checking for.

paxdiablo
The biggest problem is overflow when you do:check = d * multiplier;this doesn't work for large numbers;
tvanfosson
IEE 754 64-bit doubles only have about 18 digits precision anyway so, if you get up into numbers where multiplying by 1000 will cause overflow (about 10^304), you can be certain that there are zero decimals in the representation.
paxdiablo
+1  A: 

The double type is a binary floating point number. There are always apparent inaccuracies in dealing with them as if they were decimal floating point numbers. I don't know that you'll ever be able to write your function so that it works the way you want.

You will likely have to go back to the original source of the number (a string input perhaps) and keep the decimal representation if it is important to you.

Ned Batchelder
+5  A: 

If your goal is to represent a number with exactly n significant figures to the right of the decimal, BigDecimal is the class to use.

Immutable, arbitrary-precision signed decimal numbers. A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale. If zero or positive, the scale is the number of digits to the right of the decimal point. If negative, the unscaled value of the number is multiplied by ten to the power of the negation of the scale. The value of the number represented by the BigDecimal is therefore (unscaledValue × 10-scale).

scale can be set via setScale(int)

Ken Gentle
A: 

I'm not sure that this is really doable in general. For example, how many decimal places does 1.0e-13 have? What if it resulted from some rounding error while doing arithmetic and really is just 0 in disguise? If on, the other hand you are asking if there are any non-zero digits in the first n decimal places you can do something like:

   static boolean checkDecimalPlaces(double d, unsigned int decimalPlaces){
      // take advantage of truncation, may need to use BigInt here
      // depending on your range
      double d_abs = Math.abs(d);
      unsigned long d_i = d_abs; 
      unsigned long e = (d_abs - d_i) * Math.pow(10, decimalPlaces);
      return e > 0;
   }
tvanfosson
actually your answer helped me, but as it is now it is flawed as you have to multiply `d` before you truncate it to not lose significant digits - see my edit of the question
Turismo
+3  A: 

The test fails, because you have reached the accuracy of the binary floating point representation, which is approximately 16 digits with IEEE754 double precision. Multiplying by 649632196443.4279 by 10000 will truncate the binary representation, leading to errors when rounding and dividing afterwards, thereby invalidating the result of your function completely.

For more details see http://en.wikipedia.org/wiki/Floating_point#Accuracy_problems

A better way would be to check whether the n+1 decimal places are below a certain threshold. If d - round(d) is less than epsilon (see limit), the decimal representation of d has no significant decimal places. Similarly if (d - round(d)) * 10^n is less than epsilon, d can have at most n significant places.

Use Jon Skeet's DoubleConverter to check for the cases where d isn't accurate enough to hold the decimal places you are looking for.

David Schmitt
I also looked at the DoubleConverter Jon posted in his answer and it seems to do pretty much the same as BigDecimal. I wonder why he removed his answer.
Turismo
+1  A: 

If you can switch to BigDecimal, then as Ken G explains, that's what you should be using.

If not, then you have to deal with a host of issues as mentioned in the other answers. To me, you are dealing with a binary number (double) and asking a question about a decimal representation of that number; i.e., you are asking about a String. I think your intuition is correct.

Glenn