0

As the title states, I'm trying to convert some floats to ints and there are a few anomalies i saw for a couple of values. I have a method that's trying to convert the decimal portion of a float, for example .45 from 123.45, to a string representation where it outputs as 45/100.

The problem is that for 0.35, 0.45, 0.65 and 0.95 i get 0.34, 0.44, 0.64 and 0.94 respectively. Here's my implementation:

public String convertDecimalPartToString(float input){

    int numberOfDigits = numberOfDigits(input);

    System.out.println(numberOfDigits);

    String numerator = Integer.toString((int) (input * Math.pow(10, numberOfDigits)));

    String denominator = Integer.toString((int) Math.pow(10, numberOfDigits));

    return numerator + "/" + denominator;
}

public int numberOfDigits(float input){

    String inputAsString = Float.toString(input);

    System.out.println(inputAsString);

    int digits = 0;
    // go to less than 2 because first 2 will be 0 and .
    for(int i =0;i<inputAsString.length()-2;i++){
        digits++;
    }
    return digits;
}

My test method looks like this:

@Test
public void testConvertDecimalPartToString(){
    float input = 0.95f;
    //output is 94/100 and assert fails
    Assert.assertEquals("95/100", checkWriter.convertDecimalPartToString(input));

}

It seems like there's a miscalculation in this line: String numerator = Integer.toString((int) (input * Math.pow(10, numberOfDigits))); but I don't understand why it works properly for 0.05, 0.15, 0.25, 0.55, 0.75 and 0.85.

Can anybody help me understand?

Asad Nawaz
  • 355
  • 1
  • 3
  • 14
  • 2
    Instead of casting to int, use a function that rounds like Math.rint. – Louis Wasserman Nov 15 '20 at 18:37
  • @LouisWasserman Math.rint() returns a double I would have to cast it anyway. And i'm not trying to round any numbers off in the first place. Also, the question still remains why it works for some values and not the others. – Asad Nawaz Nov 15 '20 at 18:42
  • @LouisWasserman actually that works. Casting to int after rounding it fixes the problem. Still not sure why the casting for random numbers results in something like ```34.99999940395355``` instead of 35 – Asad Nawaz Nov 15 '20 at 18:58
  • 2
    The answer is in https://stackoverflow.com/q/588004/869736: there is fundamental inaccuracy in how floats are stored; sometimes it is inaccurately high and sometimes it is inaccurately low; and (int) rounds down, so if you get unlucky with an inaccurately low representation, you get a wrong value. The fix is *to do rounding.* – Louis Wasserman Nov 15 '20 at 18:59
  • 1
    So better to use double wherever possible instead of float – abhijit gupta Nov 15 '20 at 19:02
  • @abhijitgupta I agree but this task in particular requires either int or float inputs. I didn't want to do a whole lot of type conversions but i guess i did anyway. – Asad Nawaz Nov 15 '20 at 19:04
  • @abhijitgupta Doubles are more accurate, but that doesn't make them actually accurate. Doubles would suffer the same problem here. – Louis Wasserman Nov 15 '20 at 19:08
  • @LouisWassermanTrue. Double is represented in 64 bits whereas Float is represented in 32 bits. And java by default uses double to represent its floating numbers. So a literal 3.14 is typed as double and not float. Lesser prone to errors – abhijit gupta Nov 15 '20 at 19:16

1 Answers1

6

The problem is blessed numbers. Imagine I gave you 3 bits (0 or 1 values): you could only represent 8 different values with this: 000, 001, 010, 011, 100, 101, 110, and 111. That's all of em. Can't represent more than 8 concepts if that's the only legal values!

float is a 32-bit value. With 32 bits, I can give you up to 4 billion different values. That's a lot of values, but it is not infinite, and yet there are infinite numbers between 0 and 1, let alone between 0 and 340,282,346,638,528,860,000,000,000,000,000,000,000.000000 which is the largest value a float can represent.

So how does that work? Well, not every number is actually representable. Only about 4 billion numbers are. These are the blessed numbers.

Anytime you try to make a non-blessed number, or the result of a calculation isn't blessed, then your number is rounded to the nearest blessed number, and if you perform a sequence of operations, that rounding occurs at every step.

The nature of blessed numbers is such that there are as many blessed numbers beteen 0 and 1 as there are above 1 - as you move away from 0 the interval between any 2 blessed numbers goes up. Eventually it'll be more than 1.0, in fact.

Furthermore, computers count in binary, not decimal. Just like you cannot represent '1 divided amongst 3 things' in decimal (0.33333... it never ends), something as simple as 0.1 (1 divided amongst 10 things) cannot be perfectly represented in binary, so something as simple as 1.0/10.0 already rounds!

Your code will fail if _any_rounding occurs. The solution in your case is fairly easy; add 0.005 would do it. A better way is to first render it to a rounded string:

String x = String.format("%.02f", yourValue);

and then find what you need in the string. The above takes care of proper rounding and will do a better job than using Math.pow, which, as it moves you away from that 0, causes MORE errors to show up (further from 0 -> more extreme rounding errors, as there are fewer blessed numbers out that far).

NB: Note that double is as fast as float, and given that you have 64 bits to spend there, has way more blessed numbers, so, fewer errors.

NB2: Another way to do this is to just move your concept of a 'unit'. For example, if this represents cash, just have int cents; - no problems there, it's much easier to know what the blessed numbers are for int (every integer between -2^31 and +2^31).

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Re “there are as many blessed numbers beteen 0 and 1 as there are above 1”: There are more above 1 than below 1, by about .79%. The number of numbers representable in IEEE-754 binary32 from 0 (inclusive) to 1 (exclusive) is 127•2^23, one for each combination of an exponent code from 0 to 126 and a 23-bit significand. The number of numbers representable from 1 (exclusive) to infinity (inclusive) is 128•2^23, one for each exponent code from 127 to 254 with a significand and then minus 1 to exclude 1 plus 1 to include infinity. So the ratio of numbers above 1 to numbers 1 or below is 128:127. – Eric Postpischil Nov 15 '20 at 20:51
  • Re “computers count in binary, not decimal”: Computers count as they are programmed to count. There are decimal floating-point formats and arithmetic systems. – Eric Postpischil Nov 15 '20 at 20:52
  • Good catch, fixed that typo! – rzwitserloot Nov 15 '20 at 22:37