From b5439e01843a808de259bb94d76287ce0133c909 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 2 Jan 2025 17:39:42 +0100 Subject: [PATCH] Fix writing of numeric timestamps with negative `epochSecond` values + add support for writing numeric timestamps as JSON keys --- .../jsoniter_scala/core/JsonWriter.scala | 65 +++++++++++++++++-- .../jsoniter_scala/core/JsonWriter.scala | 65 +++++++++++++++++-- .../jsoniter_scala/core/JsonWriter.scala | 65 +++++++++++++++++-- .../jsoniter_scala/core/JsonWriterSpec.scala | 23 ++++--- version.sbt | 2 +- 5 files changed, 191 insertions(+), 29 deletions(-) diff --git a/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala b/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala index d990e5ddf..0e777831c 100644 --- a/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala +++ b/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala @@ -131,6 +131,39 @@ final class JsonWriter private[jsoniter_scala]( writeParenthesesWithColon() } + /** + * Writes a timestamp value as a JSON key. + * + * @param epochSecond the epoch second of the timestamp to write + * @param nano the nanoseconds of the timestamp to write + * @throws JsonWriterException if the nanoseconds value is less than 0 or greater than 999999999 + */ + def writeTimestampKey(epochSecond: Long, nano: Int): Unit = { + if (nano < 0 || nano > 999999999) encodeError("illegal nanoseconds value: " + nano) + writeOptionalCommaAndIndentionBeforeKey() + writeBytes('"') + var pos = ensureBufCapacity(30) + val buf = this.buf + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { + val dotPos = pos + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) + buf(dotPos) = '.' + } + this.count = pos + writeParenthesesWithColon() + } + /** * Writes a `BigInt` value as a JSON key. * @@ -766,10 +799,20 @@ final class JsonWriter private[jsoniter_scala]( writeOptionalCommaAndIndentionBeforeValue() var pos = ensureBufCapacity(30) val buf = this.buf - pos = writeLong(epochSecond, pos, buf) - if (nano != 0) { + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { val dotPos = pos - pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits) + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) buf(dotPos) = '.' } this.count = pos @@ -906,10 +949,20 @@ final class JsonWriter private[jsoniter_scala]( val buf = this.buf buf(pos) = '"' pos += 1 - pos = writeLong(epochSecond, pos, buf) - if (nano != 0) { + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { val dotPos = pos - pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits) + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) buf(dotPos) = '.' } buf(pos) = '"' 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 ce8016b1c..7f6603d65 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,39 @@ final class JsonWriter private[jsoniter_scala]( writeParenthesesWithColon() } + /** + * Writes a timestamp value as a JSON key. + * + * @param epochSecond the epoch second of the timestamp to write + * @param nano the nanoseconds of the timestamp to write + * @throws JsonWriterException if the nanoseconds value is less than 0 or greater than 999999999 + */ + def writeTimestampKey(epochSecond: Long, nano: Int): Unit = { + if (nano < 0 || nano > 999999999) encodeError("illegal nanoseconds value: " + nano) + writeOptionalCommaAndIndentionBeforeKey() + writeBytes('"') + var pos = ensureBufCapacity(30) + val buf = this.buf + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { + val dotPos = pos + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) + buf(dotPos) = '.' + } + this.count = pos + writeParenthesesWithColon() + } + /** * Writes a `BigInt` value as a JSON key. * @@ -709,10 +742,20 @@ final class JsonWriter private[jsoniter_scala]( writeOptionalCommaAndIndentionBeforeValue() var pos = ensureBufCapacity(30) val buf = this.buf - pos = writeLong(epochSecond, pos, buf) - if (nano != 0) { + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { val dotPos = pos - pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits) + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) buf(dotPos) = '.' } this.count = pos @@ -843,10 +886,20 @@ final class JsonWriter private[jsoniter_scala]( val buf = this.buf buf(pos) = '"' pos += 1 - pos = writeLong(epochSecond, pos, buf) - if (nano != 0) { + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { val dotPos = pos - pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits) + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) buf(dotPos) = '.' } buf(pos) = '"' diff --git a/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala b/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala index 9aac2ca76..6e0474028 100644 --- a/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala +++ b/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala @@ -131,6 +131,39 @@ final class JsonWriter private[jsoniter_scala]( writeParenthesesWithColon() } + /** + * Writes a timestamp value as a JSON key. + * + * @param epochSecond the epoch second of the timestamp to write + * @param nano the nanoseconds of the timestamp to write + * @throws JsonWriterException if the nanoseconds value is less than 0 or greater than 999999999 + */ + def writeTimestampKey(epochSecond: Long, nano: Int): Unit = { + if (nano < 0 || nano > 999999999) encodeError("illegal nanoseconds value: " + nano) + writeOptionalCommaAndIndentionBeforeKey() + writeBytes('"') + var pos = ensureBufCapacity(30) + val buf = this.buf + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { + val dotPos = pos + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) + buf(dotPos) = '.' + } + this.count = pos + writeParenthesesWithColon() + } + /** * Writes a `BigInt` value as a JSON key. * @@ -709,10 +742,20 @@ final class JsonWriter private[jsoniter_scala]( writeOptionalCommaAndIndentionBeforeValue() var pos = ensureBufCapacity(30) val buf = this.buf - pos = writeLong(epochSecond, pos, buf) - if (nano != 0) { + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { val dotPos = pos - pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits) + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) buf(dotPos) = '.' } this.count = pos @@ -843,10 +886,20 @@ final class JsonWriter private[jsoniter_scala]( val buf = this.buf buf(pos) = '"' pos += 1 - pos = writeLong(epochSecond, pos, buf) - if (nano != 0) { + var es = epochSecond + var ns = nano + if (es < 0 & ns > 0) { + es += 1 + ns = 1000000000 - ns + if (es == 0) { + buf(pos) = '-' + pos += 1 + } + } + pos = writeLong(es, pos, buf) + if (ns != 0) { val dotPos = pos - pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits) + pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits) buf(dotPos) = '.' } buf(pos) = '"' diff --git a/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriterSpec.scala b/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriterSpec.scala index 60acaec9f..b1e636533 100644 --- a/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriterSpec.scala +++ b/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriterSpec.scala @@ -737,18 +737,21 @@ class JsonWriterSpec extends AnyWordSpec with Matchers with ScalaCheckPropertyCh "JsonWriter.writeVal for a timestamp" should { "write timestamp values" in { def check(epochSecond: Long, nano: Int): Unit = { - val s = BigDecimal({ - val es = java.math.BigDecimal.valueOf(epochSecond) - if (nano == 0) es - else es.add(java.math.BigDecimal.valueOf({ - if (epochSecond < 0) -nano - else nano - }.toLong, 9).stripTrailingZeros) - }).toString - withWriter(_.writeTimestampVal(epochSecond, nano)) shouldBe s - withWriter(_.writeTimestampValAsString(epochSecond, nano)) shouldBe s""""$s"""" + val s = + if (nano == 0) epochSecond.toString + else BigDecimal({ + java.math.BigDecimal.valueOf(epochSecond) + .add(java.math.BigDecimal.valueOf(nano.toLong, 9).stripTrailingZeros) + }).toString + if (!s.contains("E")) { + withWriter(_.writeTimestampVal(epochSecond, nano)) shouldBe s + withWriter(_.writeTimestampValAsString(epochSecond, nano)) shouldBe s""""$s"""" + withWriter(_.writeTimestampKey(epochSecond, nano)) shouldBe s""""$s":""" + } } + check(-1L, 123456789) + check(-1L, 0) check(1L, 0) check(1L, 900000000) check(1L, 990000000) diff --git a/version.sbt b/version.sbt index d7c55e60c..77926e264 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "2.32.1-SNAPSHOT" +ThisBuild / version := "2.33.0-SNAPSHOT"