reugn / scalikejackson

Lightweight Scala JSON library
Apache License 2.0
7 stars 1 forks source link

Loosing parsing of float values #12

Closed plokhotnyuk closed 5 years ago

plokhotnyuk commented 5 years ago

Current code from the master branch parses floats with rounding where error ~1ULP that is greater than expected ~0.5ULP of java.lang.Float.valueOf().

The rounding error can be easy reproduced when parsing string representation of some values with number of digits greater than usually used for floats.

scala> "1.00000017881393432617187499".toFloat
res0: Float = 1.0000001

scala> "1.00000017881393432617187499".toDouble.toFloat
res1: Float = 1.0000002

The detailed explanation is in this comment

The following code can print lot of such numbers after increasing number of iterations:

sbt:jsoniter-scala-benchmark> console
[info] Compiling 1 Scala source to /home/andriy/Projects/com/github/plokhotnyuk/jsoniter-scala/jsoniter-scala-benchmark/target/scala-2.12/classes ...
[info] Starting scala interpreter...
Welcome to Scala 2.12.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_212).
Type in expressions for evaluation. Or try :help.

scala> :paste
// Entering paste mode (ctrl-D to finish)

    import reug.scalikejackson.ScalaJacksonImpl._
    import reug.scalikejackson.play.Json

    implicit val arrayOfFloatsFormatter: reug.scalikejackson.ScalaJacksonFormatter[Float] = Json.format()

    (1 to 1000).foreach { _ =>
      def checkAndPrint(input: String): Unit = {
        val actualOutput = input.read[Float]
        val expectedOutput = input.toFloat
        if (actualOutput != expectedOutput) {
          println(s"input = $input, expectedOutput =$expectedOutput, actualOutput = $actualOutput")
        }
      }

      val n = java.util.concurrent.ThreadLocalRandom.current().nextLong()
      val x = java.lang.Double.longBitsToDouble(n & ~0xFFFFFFFL)
      if (java.lang.Float.isFinite(x.toFloat) && x.toFloat != 0.0) checkAndPrint(x.toString)
    }

// Exiting paste mode, now interpreting.

input = 7.877632165348009E31, expectedOutput =7.877632E31, actualOutput = 7.8776324E31
input = -1.2859984345895449E-24, expectedOutput =-1.2859984E-24, actualOutput = -1.2859985E-24
input = 7.279773797108646E-7, expectedOutput =7.2797735E-7, actualOutput = 7.279774E-7
input = -3.266754557016287E-24, expectedOutput =-3.2667545E-24, actualOutput = -3.2667547E-24
input = -1.215417364698015E-34, expectedOutput =-1.2154174E-34, actualOutput = -1.2154173E-34
input = -0.004661555634811521, expectedOutput =-0.004661556, actualOutput = -0.0046615554
input = 5.412722892117472E-8, expectedOutput =5.412723E-8, actualOutput = 5.4127227E-8
input = -1.9493489154075532E37, expectedOutput =-1.9493489E37, actualOutput = -1.949349E37
input = -5.331430234350818E-35, expectedOutput =-5.33143E-35, actualOutput = -5.3314305E-35
...

I see that ScalikeJackson behave exactly as original Jackson, but could you, please, consider documenting this rounding if there are no other option available.

And, also, because it differs how Play-JSON parse floats in its latest release:

scala> play.api.libs.json.Json.parse("7.877632165348009E31").as[Float]
res0: Float = 7.877632E31
plokhotnyuk commented 5 years ago

I have tried v0.5.4 but rounding still occurs for some values.

Inputs:

1.199999988079071
3.4028235677973366e38
7.006492321624086e-46

Expected output:

1.1999999
3.4028235E38
1.4E-45

Actual output:

1.2
Infinity
0.0