views:

509

answers:

6

I'm working on a Java/Groovy program. I have a double variable that holds a number that was typed in by a user. What I really want to know is how many numbers the user typed to the right of the decimal place. Something like:

double num = 3.14
num.getPlaces() == 2

Of course, you can't do this with a double since that's using IEEE floating points and it's all an approximation.

Assuming that I can't get at the string the user typed, but only have access to the double the value has been stored in, is there a way I can scrub that double though a BigDecimal or somesuch to get the "real" number of decimal places? (When the double gets displayed on the screen, it gets it right, so I assume there is a way to at least guess well?)

+12  A: 

No, you can't... because there are lots of different strings the user could have typed in which would all be parsed to the same value.

The "real" number is almost certain to have more decimal places than the user typed. For example, 3.14 is stored as exactly 3.140000000000000124344978758017532527446746826171875. I don't think you want to display that to the user.

To make the problem clearer, you can't tell from the double value whether the user actually entered:

3.14
3.140
3.140000
3.14000000000000012434

Those four strings will all give you the same value - which should make it obvious that you can't possibly get back to what the user typed in.

If at all possible, change the parsing code and use BigDecimal throughout.

Jon Skeet
Nitpick: If you're using 3.14 in your code, you should consider using `Math.PI` instead.
R. Bemrose
@R.Bemrose: Do you believe the number 3.14 never ever comes up anywhere without it actually meaning pi?
Jon Skeet
lol @ use pi instead, possibly not the answer he's looking for...
Andrew G. Johnson
Guys 3.14 was just an example. We're talking about arbitrary user entered doubles.
Electrons_Ahoy
And, for the record, in the world where I am King (or, at least, Project Manager) all those doubles would become BigDecimals. Sadly, I think that change might be received as being a little out of scope. ;)
Electrons_Ahoy
3.14 an PI - that reminds me of the famous Indiana Pi Bill: http://en.wikipedia.org/wiki/Indiana_Pi_Bill
Andreas_D
+1  A: 

You are right there is something strange going on with doubles, what gets printed out is not the same as the contents of the variable.

for example:

groovy:000> "123.0001".toBigDecimal()
===> 123.0001
groovy:000> "123.0001".toDouble()
===> 123.0001
groovy:000> new BigDecimal("123.0001".toDouble())
===> 123.000100000000003319655661471188068389892578125

Notice the damage is done in the conversion of the string to a double, not when the double is passed into the BigDecimal. Feeding the double to the BigDecimal just provides an easy way to see what's actually in the double, because toString is lying to you.

As Jon Skeet points out, accuracy is not an option here. However, assuming the value printed out on the screen is the result of calling toString on the double, you should be able to get a bigDecimal that's no more wrong than the toString version of the double, like this:

groovy:000> d = "123.0001".toDouble()
===> 123.0001
groovy:000> d.toString()
===> 123.0001
groovy:000> new BigDecimal(d.toString())
===> 123.0001

So you don't need to involve the BigDecimal, really, you can just do something like

groovy:000> d = 123.0001
===> 123.0001
groovy:000> s = d.toString()
===> 123.0001
groovy:000> s.substring(s.indexOf('.')).length() - 1
===> 4

Sorry to invalidate your comment by editing.

BTW here's something close to Steve's answer, translated to groovy. (I took out the test for the decimal point not found because if you run this on a machine with locales messed up so it isn't using a period for the decimal point I'd rather it blow up than return 0)

def getPlaces(d) {
    s = d.toString()
    s.substring(s.indexOf(".")).length() - 1
}
Nathan Hughes
Believe me, I wouldn't if I had the option. But, you know, existing codebase and all that. I'm not even working on the module that collects the data, so I just get a double handed to me. :)
Electrons_Ahoy
Sure, I had to write code interfacing with an Amdocs system that kept all monetary information as doubles, so I had the same put-the-toothpaste-back-in-the-tube problem. I think all you can do is convert it back to a string, truncate it back to what the input data was originally, and hope it's not too far off.
Nathan Hughes
Man, at least mine isn't fiscal numbers, so the occasional rounding error is okay.
Electrons_Ahoy
"Notice the damage is done in the conversion of the string to a double..." is an interesting turn of phrase. For the casual reader, the 'damage' is that double is only an approximation. A large majority of the numbers between min double and max double are not representable. It does the best it can. :) Double.toString() attempts to reapproximate what we mean since it is more likely that we meant 3.14 instead of 3.1400000000000001243... and so on.
PSpeed
Of course the double offers only an approximation. The problem is that in this case what is needed isn't an approximation, it's the actual value the user entered, which has possibly been lost. Using the wrong tool for the job can result in damage.
Nathan Hughes
A: 

if you're stuck with a double, convert to a string and count the number of characters after the decimal point. I think there is some magic involved that displays numbers like 1.99999999998 as "2"

Jimmy
A: 

so if it is a user entered value. Accept the value as a String. Then you can use string functions to find the position of the "." and then subtract that number from the length of the string to get the number your looking for. Of course you'll want to trim() it, and verify it is in fact a number that was entered..

Jay
the question specifically mentions not having access to the string, only the double value.
Peter Recore
A: 

If you absolutely must take a double, and you don't mind having a little utility method to run it through, you could write something like this. You said you don't have access to the String value of what the user entered, but is there a reason why you can't convert it to a string and do something like this?

static int getPlaces(double num) {
    String numString = String.valueOf(num);

    return numString.indexOf(".0")==numString.length()-2?0:numString.length()-numString.indexOf(".")-1;
}

then, instead of your example

 num.getPlaces() == 2

You can do

 getplaces(num) == 2

I hope this helps..

Update, given your comment That the user entered. Good point.

If the user entered what looks like an integer (say, 5) without a decimal point, and you're receiving it as a double, then what you're going to get is something other than what the user entered - for s/he will have entered 5, but you'll have received 5.0. You'd have no way of telling whether the user actually entered 5 or 5.0.

steve
BTW, sorry that I don't have a groovy way of writing that method. I only know java..
steve
A: 

Yes, you can. At least if you don't mind that you sometimes get a wrong result. We must assume that the user is lazy and has entered no more than ~12 significant decimal digits.

Then do the following:

  • Create a BigDecimal with the given double in the constructor. That will convert the double in an exact decimal representation.

  • Get only the fraction.

  • Get BigDecimal.toString() and find three or more consecutive '0' or '9' digits.

  • Cut off the fraction after those three digits. If the digits are nine, add a "1" at the end. After that remove all trailing zeroes.

  • Count the remaining fractional digits

Warning: This may be a bit slow.

If you only want the number of significant bits in the double you can get them much faster and easier, but decimal->binary conversion unfortunately uses almost all bits.

Thorsten S.