Search code examples
javamathrounding

Confused with variable types and Math.round


double n = 123.4567, o = 123.4567, p = 123.4567;
n *= 100;
n = Math.round(n);
n = n / 100;

System.out.println(n);

System.out.println(Math.round(o * 100) / 100);

System.out.println(((double) Math.round(p * 100)) / 100);

I was playing around with easy ways to round a decimal number to 2 points and found this.

Output:

123.46
123
123.46

Is there a difference between the first and second approaches that I'm just not seeing? They should be identical algorithms, just compressed into one line. Why is a cast needed when it's all in one line and not needed when in separate lines?


Solution

  • You're missing two important things:

    • This isn't at all how you should deal with rounding.
    • Nevertheless you may still be interested in an answer to your question. Which is: / means different things depending on context.

    / means different things

    The key thing you're missing is that / means 2 different things in java; context determines which of the 2 you get. In your first and second snippets, the / mean different things.

    In the first and third, it means 'double division'. In the second, it means 'integer division'.

    These 2 things are as different as guns and grandmas. Consider how + means completely different things. Given a + b, well:

    String a = "Hello";
    String b = "World";
    String c = a + b; // means string concatenation
    
    int a = 5;
    int b = 7;
    int c = a + b; // means integer addition
    

    You should consider / to be the same way.

    In general all java math operations are homogenous. Meaning, java knows how to add an 'int' to an 'int' but cannot deal with the situation when the two arguments aren't of the same type. To 'fix' that, you will have to convert one of the two sides to be the same type as the other side. To make things a bit complicated, javac itself will assume you meant to do just that and will convert one of them for you, if it can: The rulebook states that java will only apply widening conversions - once that are generally considered lossless. For example, converting an int to a double is considered lossless. Converting an int to a short is not (a short can contain fewer values than int can).

    Given:

    int a = 11;
    int b = 10;
    double c = a / b;
    System.out.println(c);
    

    You might think that prints "1.1". It will not. Try it - run that. It'll print simply '1'.

    That's because That slash there means integer division: Because both of its arguments are of type int, int division is what it means. The fact that you then assign the result of this to a variable of type double is not relevant to this determination. So, a / b is integer division, in integer division, all remainder is just lopped off (so, rounding down for positive numbers but rounding up for negative numbers, -5.5 becomes -5), producing just 1. This is then assigned to a double which requires conversion. Because that is 'widening', java injects the cast for you silently, there is no need to explicitly write it, and c is now 1.0.

    The same is happening your code: The second snippet is dividing an integer by an integer (Because the return type of the round method in the Math class is int), thus, you get integer math.

    When you use / and the operands are of mixed types, one of type double and the other of int, java will silently widening-convert the int to a double, so that just 'works':

    double x = 11.0;
    int y = 10;
    double z = x / y;
    System.out.println(z); // really will print '1.1'
    

    Hence why a cast can be necessary:

    int x = 11;
    int y = 10;
    double z = (double) x / y;
    System.out.println(z); // really will print '1.1'
    

    Here the / expression's left hand side is the expression (double) x which is of type double, the RHS is of type int, so that's not possible without conversion - but int can be silently converted to double so java compiles that as if you wrote: (double) x / (double) y which is double division, and that whole expression is of type double, so can be assigned verbatim to z.

    .. but don't do any of this

    doubles are imprecise. In weird ways. If I ask you to write the result of 1 divided by 3 on a small piece of paper you cannot do this - you write 0.33333 and run out of room.

    Computers are the same way: They have limited space (64 bit, for double), and store decimals and not ratios (you can't write "1 / 3" in this system, only "0.3333"). Except, computers don't use decimal, they use binary.

    The reason "1 / 10" 'works' (It's 0.1, no infinite sequence of digits) is because the divisors of 10 are 2 and 5, so any divisor that can be expressed solely as multiplying any number of 2s and 5s together 'works' (10 = 2 * 5 - that works), and anything else won't.

    In binary, it's just multiples of 2.

    This means 0.1 is not a double and causes rounding errors. Try it! Write a java file that just does this:

    double v = 0.0;
    for (int i = 0; i < 10; i++) v += 0.1;
    System.out.println(1.0 == v);
    

    And you will marvel! Because that prints false and that seems incorrect. It's 'correct' in the same sense that if I ask you to write 1/3 on a piece of paper three times and I then ask you to tell me the sum of those 3 pieces of paper, you won't tell me 1. No, you'd tell me '0.999999'. Which is very close to, but not quite equal to, 1.0.

    Hence, you cannot think of double in this way - they cannot be used to hold exactly 'some number rounded to 2 decimals' because a thing that works in decimal (such as "0.10") may not work in binary.

    Instead, you have 3 options:

    Atomicity

    Whatever you're doing, find out if it has a workable atomic unit and just use those. For example, if doing finances, don't store the price of an object that costs 4 bucks and 20 cents as double price = 4.2;. No, store it as int priceInCents = 420.

    But, not everything has an atomic unit.

    Accept the error

    Accept that slight rounding errors will occur. NEVER try to round a number itself, instead, when printing it, round then:

    double v = 1.2345; // v's value is not quite 1.2345, but something very close.
    System.out.printf("%.2f\n", v);
    

    That prints 1.23. Because '%.2f' rounds to 2 digits. Don't round the value, ask the thing that prints to round.

    BigDecimal

    Use java's BigDecimal class which represents decimal numbers to any precision you want. But, this does mean:

    • It's very slow and hard to use.
    • Memory requirements grow as your number gains digits.
    • It still cannot solve the 1 / 3 dilemma. That division will fail, unless you tell BigDecimal how to round it.

    Generally BD is overkill. Don't use it unless you really, really know what you are doing.

    Sooo.. finance stuff?

    Always use long to represent your finances (unless you really really know what you are doing and think BD is a better idea. It rarely is). No matter what happens, double is never the correct data type for finance.

    And, of course, represent atomics. So, cents for euros and dollas, yen for yen, satoshis for bitcoin, pennies for pounds, and so on.