Wrong comparing of Double and Int numbers in Kotlin (bug perhaps)

I’m working on calculator project but when I tried to use two similar ways of comparing double and int numbers I got different results. So my question is Why are these ways of comparing works differently?

//some code that parse the string
//...
//code of calculating:

fun calculateIn(queueNumbers: Queue<Double>, queueActions: Queue<Char>) {
var action: Char
var result = queueNumbers.poll()
var operand: Double

while (!queueNumbers.isEmpty()) {
    operand = queueNumbers.poll()
    action = queueActions.poll()
    when (action) {
        '-' -> result -= operand
        '+' -> result += operand
        '*' -> result *= operand
        '/' -> result /= operand
        '%' -> result = result % operand * -1.0
    }
  }
  var pointNum = 8.3

  println("pointNum = " + pointNum)
  println(if(pointNum.compareTo(pointNum.toInt()) == 0) pointNum.toInt() else pointNum)

  println("result = " + result)
  println(if(result.compareTo(result.toInt()) == 0) result.toInt() else result)
}

Result of code:

"10.3 + -2" //input String

[10.3, -2.0] //queueNumbers

[+]//queueActions

pointNum = 8.3

8.3

result = 8.3

8

I think that is strange because if I run similar code I will get the correct result:

var pointNum = 8.3

println(if(pointNum.compareTo(pointNum.toInt()) == 0) pointNum.toInt() else pointNum)

So there is result of this code:

8.3

Full code on GitHub: GitHub - Trilgon/LearningKotlin: The part of code with bug in Kotlin because of wrong bytecode generated (thanks "broot" for report to JetBrains)

The problem is that doubles and ints are fundamentally different types. If you call toInt() on a Double this will loose data (drop the decimal places).

It is “better” to compare as doubles. To make things worse, however, when comparing doubles for equality you generally should do a comparison in a given precision (as floating point math introduces errors). If you want precision, you use BigDecimal instead, this type has “unlimited” precision and will not loose precision, but can lead to large numbers.

2 Likes

I generally agree with what @pdvrieze said, but still, this looks like a bug in the compiler to me.

Minimal reproducible example:

fun test1(): Int {
    val d: Double?
    d = 8.3
    return d.compareTo(8) // 0
}

fun test2(): Int {
    val d: Double
    d = 8.3
    return d.compareTo(8) // 1
}

Technical difference between these two code samples is that the value is boxed in test1() and unboxed in test2(). If we look at the generated bytecode for test2(), everything looks as expected:

       7: bipush        8
       9: i2d
      10: invokestatic  #63                 // Method java/lang/Double.compare:(DD)I

It converts integer value 8 to double and then compares both values as doubles.

But if we look into test1(), something bizarre happens there:

      10: invokevirtual #52                 // Method java/lang/Double.doubleValue:()D
      13: d2i
      14: bipush        8
      16: invokestatic  #58                 // Method kotlin/jvm/internal/Intrinsics.compare:(II)I

It does the opposite: it converts double value 8.3 to integer and then compares values as integers. This is why it says values are the same.

What is interesting, even “Kotlin Bytecode” tool in IntelliJ shows the correct code for test1():

    INVOKEVIRTUAL java/lang/Double.doubleValue ()D
    BIPUSH 8
    I2D
    INVOKESTATIC java/lang/Double.compare (DD)I

But the real generated bytecode is different and gives different results.

edit:
I reported this problem: https://youtrack.jetbrains.com/issue/KT-52163

4 Likes

Thanks, I will keep in mind about precision next time

I had some thoughts about problem with compiler but I wasn’t sure how to check it. Thanks for your detailed answer and report for JetBrains.