From 4809efc8adf99eda808d91ec4c5c2e6cea27eaf1 Mon Sep 17 00:00:00 2001 From: plokhotnyuk Date: Sun, 28 Jan 2024 19:50:52 +0100 Subject: [PATCH] Add writing of Decimal64 from underlying 64-bit representation --- build.sbt | 1 + .../jsoniter_scala/core/JsonWriter.scala | 72 +++++++++++++++ .../jsoniter_scala/core/Decimal64Spec.scala | 88 +++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 jsoniter-scala-core/jvm/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/Decimal64Spec.scala diff --git a/build.sbt b/build.sbt index 1feb1ecea..7fc916993 100644 --- a/build.sbt +++ b/build.sbt @@ -144,6 +144,7 @@ lazy val `jsoniter-scala-core` = crossProject(JVMPlatform, JSPlatform, NativePla .settings( crossScalaVersions := Seq("3.3.1", "2.13.12", "2.12.18"), libraryDependencies ++= Seq( + "com.epam.deltix" % "dfp" % "1.0.2" % Test, "org.scala-lang.modules" %%% "scala-collection-compat" % "2.11.0" % Test, "org.scalatestplus" %%% "scalacheck-1-17" % "3.2.17.0" % Test, "org.scalatest" %%% "scalatest" % "3.2.17" % Test diff --git a/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala b/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala index 111f5d9f3..a022d8cbd 100644 --- a/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala +++ b/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala @@ -131,6 +131,19 @@ final class JsonWriter private[jsoniter_scala]( writeParenthesesWithColon() } + /** + * Writes an underlying Decimal64 representation as a JSON key. + * + * @param x the underlying Decimal64 representation + * @throws JsonWriterException if the value is non-finite + */ + def writeDecimal64Key(x: Long): Unit = { + writeOptionalCommaAndIndentionBeforeKey() + writeBytes('"') + writeDecimal64(x) + writeParenthesesWithColon() + } + /** * Writes a `BigInt` value as a JSON key. * @@ -697,6 +710,17 @@ final class JsonWriter private[jsoniter_scala]( writeDouble(x) } + /** + * Writes an underlying Decimal64 representation as a JSON number. + * + * @param x the underlying Decimal64 representation + * @throws JsonWriterException if the value is non-finite + */ + def writeDecimal64Val(x: Long): Unit = { + writeOptionalCommaAndIndentionBeforeValue() + writeDecimal64(x) + } + /** * Writes a `BigDecimal` value as a JSON string value. * @@ -808,6 +832,19 @@ final class JsonWriter private[jsoniter_scala]( writeBytes('"') } + /** + * Writes an underlying Decimal64 representation as a JSON number. + * + * @param x the underlying Decimal64 representation + * @throws JsonWriterException if the value is non-finite + */ + def writeDecimal64ValAsString(x: Long): Unit = { + writeOptionalCommaAndIndentionBeforeValue() + writeBytes('"') + writeDecimal64(x) + writeBytes('"') + } + /** * Writes a byte array as a JSON hexadecimal string value. * @@ -2372,6 +2409,39 @@ final class JsonWriter private[jsoniter_scala]( count = pos } + private[this] def writeDecimal64(x: Long): Unit = { + var pos = ensureBufCapacity(22) + val buf = this.buf + var m10 = x & 0x001FFFFFFFFFFFFFL + var e10 = (x >> 53).toInt + if ((x & 0x6000000000000000L) == 0x6000000000000000L) { + if ((x & 0x7800000000000000L) == 0x7800000000000000L) illegalDecimal64NumberError(x) + m10 = (x & 0x0007FFFFFFFFFFFFL) | 0x0020000000000000L + if (m10 > 9999999999999999L) m10 = 0 + e10 = (x >> 51).toInt + } + e10 = (e10 & 0x3FF) - 398 + if (x < 0) { + buf(pos) = '-' + pos += 1 + } + pos = writeLong(m10, pos, buf) + if (e10 != 0) { + ByteArrayAccess.setShort(buf, pos, 0x2D65) + pos += 1 + if (e10 < 0) { + e10 = -e10 + pos += 1 + } + if (e10 < 10) { + buf(pos) = (e10 + '0').toByte + pos += 1 + } else if (e10 < 100) pos = write2Digits(e10, pos, buf, digits) + else pos = write3Digits(e10, pos, buf, digits) + } + count = pos + } + private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1) Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 @@ -2449,6 +2519,8 @@ final class JsonWriter private[jsoniter_scala]( private[this] def illegalNumberError(x: Double): Nothing = encodeError("illegal number: " + x) + private[this] def illegalDecimal64NumberError(x: Long): Nothing = encodeError("illegal Decimal64 number: " + x) + private[this] def ensureBufCapacity(required: Int): Int = { val pos = count if (pos + required <= limit) pos diff --git a/jsoniter-scala-core/jvm/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/Decimal64Spec.scala b/jsoniter-scala-core/jvm/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/Decimal64Spec.scala new file mode 100644 index 000000000..8ca2a5493 --- /dev/null +++ b/jsoniter-scala-core/jvm/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/Decimal64Spec.scala @@ -0,0 +1,88 @@ +package com.github.plokhotnyuk.jsoniter_scala.core + +import org.scalacheck.Arbitrary.arbitrary +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import com.epam.deltix.dfp.Decimal64Utils +import com.github.plokhotnyuk.jsoniter_scala.core.GenUtils._ + +import java.io._ +import java.nio.charset.StandardCharsets.UTF_8 +import scala.util.Random + +class Decimal64Spec extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks { + "JsonWriter.writeDecimal64Val and JsonWriter.writeDecimal64ValAsString and JsonWriter.writeDecimal64Key for an underlying representation of Decimal64" should { + "write finite Decimal64 values" in { + def check(n: Long): Unit = { + val s = withWriter(_.writeDecimal64Val(n)) + print(s + " ") + Decimal64Utils.compareTo(Decimal64Utils.parse(s), n) shouldBe 0 // no data loss when parsing by JVM, Native or JS Platform + s.length should be <= 22 // length is 22 bytes or less + withWriter(_.writeDecimal64ValAsString(n)) shouldBe s""""$s"""" + withWriter(_.writeDecimal64Key(n)) shouldBe s""""$s":""" + } + + check(Decimal64Utils.ZERO) + check(Decimal64Utils.ONE) + check(Decimal64Utils.TEN) + check(Decimal64Utils.THOUSAND) + check(Decimal64Utils.MILLION) + check(Decimal64Utils.ONE_TENTH) + check(Decimal64Utils.ONE_HUNDREDTH) + check(Decimal64Utils.parse("1000.0")) + check(Decimal64Utils.parse("1000.001")) + forAll(arbitrary[Long], minSuccessful(10000)) { n => + whenever(Decimal64Utils.isFinite(n)) { + check(n) + } + } + forAll(genFiniteDouble, minSuccessful(10000)) { d => + check(Decimal64Utils.fromDouble(d)) + } + forAll(arbitrary[Long], minSuccessful(10000)) { l => + check(Decimal64Utils.fromFixedPoint(l >> 8, l.toByte)) + } + forAll(arbitrary[Long], minSuccessful(10000)) { l => + check(Decimal64Utils.fromLong(l)) + } + forAll(arbitrary[Int], minSuccessful(10000)) { i => + check(Decimal64Utils.fromInt(i)) + } + } + "throw i/o exception on non-finite numbers" in { + forAll(arbitrary[Long], minSuccessful(100)) { n => + whenever(Decimal64Utils.isNonFinite(n)) { + assert(intercept[JsonWriterException](withWriter(_.writeDecimal64Val(n))).getMessage.startsWith("illegal Decimal64 number")) + assert(intercept[JsonWriterException](withWriter(_.writeDecimal64ValAsString(n))).getMessage.startsWith("illegal Decimal64 number")) + assert(intercept[JsonWriterException](withWriter(_.writeDecimal64Key(n))).getMessage.startsWith("illegal Decimal64 number")) + } + } + } + } + + def reader(json: String, totalRead: Long = 0): JsonReader = reader2(json.getBytes(UTF_8), totalRead) + + def reader2(jsonBytes: Array[Byte], totalRead: Long = 0): JsonReader = + new JsonReader(new Array[Byte](Random.nextInt(20) + 12), // 12 is a minimal allowed length to test resizing of the buffer + 0, 0, -1, new Array[Char](Random.nextInt(32)), null, new ByteArrayInputStream(jsonBytes), totalRead, readerConfig) + + def readerConfig: ReaderConfig = ReaderConfig + .withPreferredBufSize(Random.nextInt(20) + 12) // 12 is a minimal allowed length to test resizing of the buffer + .withPreferredCharBufSize(Random.nextInt(32)) + .withThrowReaderExceptionWithStackTrace(true) + + def withWriter(f: JsonWriter => Unit): String = + withWriter(WriterConfig.withPreferredBufSize(1).withThrowWriterExceptionWithStackTrace(true))(f) + + def withWriter(cfg: WriterConfig)(f: JsonWriter => Unit): String = { + val writer = new JsonWriter(new Array[Byte](Random.nextInt(16)), 0, 0, 0, false, false, null, null, cfg) + new String(writer.write(new JsonValueCodec[String] { + override def decodeValue(in: JsonReader, default: String): String = "" + + override def encodeValue(x: String, out: JsonWriter): Unit = f(writer) + + override val nullValue: String = "" + }, "", cfg), "UTF-8") + } +}