Skip to content

Commit b5439e0

Browse files
committed
Fix writing of numeric timestamps with negative epochSecond values + add support for writing numeric timestamps as JSON keys
1 parent 375a831 commit b5439e0

File tree

5 files changed

+191
-29
lines changed

5 files changed

+191
-29
lines changed

jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala

+59-6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,39 @@ final class JsonWriter private[jsoniter_scala](
131131
writeParenthesesWithColon()
132132
}
133133

134+
/**
135+
* Writes a timestamp value as a JSON key.
136+
*
137+
* @param epochSecond the epoch second of the timestamp to write
138+
* @param nano the nanoseconds of the timestamp to write
139+
* @throws JsonWriterException if the nanoseconds value is less than 0 or greater than 999999999
140+
*/
141+
def writeTimestampKey(epochSecond: Long, nano: Int): Unit = {
142+
if (nano < 0 || nano > 999999999) encodeError("illegal nanoseconds value: " + nano)
143+
writeOptionalCommaAndIndentionBeforeKey()
144+
writeBytes('"')
145+
var pos = ensureBufCapacity(30)
146+
val buf = this.buf
147+
var es = epochSecond
148+
var ns = nano
149+
if (es < 0 & ns > 0) {
150+
es += 1
151+
ns = 1000000000 - ns
152+
if (es == 0) {
153+
buf(pos) = '-'
154+
pos += 1
155+
}
156+
}
157+
pos = writeLong(es, pos, buf)
158+
if (ns != 0) {
159+
val dotPos = pos
160+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
161+
buf(dotPos) = '.'
162+
}
163+
this.count = pos
164+
writeParenthesesWithColon()
165+
}
166+
134167
/**
135168
* Writes a `BigInt` value as a JSON key.
136169
*
@@ -766,10 +799,20 @@ final class JsonWriter private[jsoniter_scala](
766799
writeOptionalCommaAndIndentionBeforeValue()
767800
var pos = ensureBufCapacity(30)
768801
val buf = this.buf
769-
pos = writeLong(epochSecond, pos, buf)
770-
if (nano != 0) {
802+
var es = epochSecond
803+
var ns = nano
804+
if (es < 0 & ns > 0) {
805+
es += 1
806+
ns = 1000000000 - ns
807+
if (es == 0) {
808+
buf(pos) = '-'
809+
pos += 1
810+
}
811+
}
812+
pos = writeLong(es, pos, buf)
813+
if (ns != 0) {
771814
val dotPos = pos
772-
pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits)
815+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
773816
buf(dotPos) = '.'
774817
}
775818
this.count = pos
@@ -906,10 +949,20 @@ final class JsonWriter private[jsoniter_scala](
906949
val buf = this.buf
907950
buf(pos) = '"'
908951
pos += 1
909-
pos = writeLong(epochSecond, pos, buf)
910-
if (nano != 0) {
952+
var es = epochSecond
953+
var ns = nano
954+
if (es < 0 & ns > 0) {
955+
es += 1
956+
ns = 1000000000 - ns
957+
if (es == 0) {
958+
buf(pos) = '-'
959+
pos += 1
960+
}
961+
}
962+
pos = writeLong(es, pos, buf)
963+
if (ns != 0) {
911964
val dotPos = pos
912-
pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits)
965+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
913966
buf(dotPos) = '.'
914967
}
915968
buf(pos) = '"'

jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala

+59-6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,39 @@ final class JsonWriter private[jsoniter_scala](
131131
writeParenthesesWithColon()
132132
}
133133

134+
/**
135+
* Writes a timestamp value as a JSON key.
136+
*
137+
* @param epochSecond the epoch second of the timestamp to write
138+
* @param nano the nanoseconds of the timestamp to write
139+
* @throws JsonWriterException if the nanoseconds value is less than 0 or greater than 999999999
140+
*/
141+
def writeTimestampKey(epochSecond: Long, nano: Int): Unit = {
142+
if (nano < 0 || nano > 999999999) encodeError("illegal nanoseconds value: " + nano)
143+
writeOptionalCommaAndIndentionBeforeKey()
144+
writeBytes('"')
145+
var pos = ensureBufCapacity(30)
146+
val buf = this.buf
147+
var es = epochSecond
148+
var ns = nano
149+
if (es < 0 & ns > 0) {
150+
es += 1
151+
ns = 1000000000 - ns
152+
if (es == 0) {
153+
buf(pos) = '-'
154+
pos += 1
155+
}
156+
}
157+
pos = writeLong(es, pos, buf)
158+
if (ns != 0) {
159+
val dotPos = pos
160+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
161+
buf(dotPos) = '.'
162+
}
163+
this.count = pos
164+
writeParenthesesWithColon()
165+
}
166+
134167
/**
135168
* Writes a `BigInt` value as a JSON key.
136169
*
@@ -709,10 +742,20 @@ final class JsonWriter private[jsoniter_scala](
709742
writeOptionalCommaAndIndentionBeforeValue()
710743
var pos = ensureBufCapacity(30)
711744
val buf = this.buf
712-
pos = writeLong(epochSecond, pos, buf)
713-
if (nano != 0) {
745+
var es = epochSecond
746+
var ns = nano
747+
if (es < 0 & ns > 0) {
748+
es += 1
749+
ns = 1000000000 - ns
750+
if (es == 0) {
751+
buf(pos) = '-'
752+
pos += 1
753+
}
754+
}
755+
pos = writeLong(es, pos, buf)
756+
if (ns != 0) {
714757
val dotPos = pos
715-
pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits)
758+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
716759
buf(dotPos) = '.'
717760
}
718761
this.count = pos
@@ -843,10 +886,20 @@ final class JsonWriter private[jsoniter_scala](
843886
val buf = this.buf
844887
buf(pos) = '"'
845888
pos += 1
846-
pos = writeLong(epochSecond, pos, buf)
847-
if (nano != 0) {
889+
var es = epochSecond
890+
var ns = nano
891+
if (es < 0 & ns > 0) {
892+
es += 1
893+
ns = 1000000000 - ns
894+
if (es == 0) {
895+
buf(pos) = '-'
896+
pos += 1
897+
}
898+
}
899+
pos = writeLong(es, pos, buf)
900+
if (ns != 0) {
848901
val dotPos = pos
849-
pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits)
902+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
850903
buf(dotPos) = '.'
851904
}
852905
buf(pos) = '"'

jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala

+59-6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,39 @@ final class JsonWriter private[jsoniter_scala](
131131
writeParenthesesWithColon()
132132
}
133133

134+
/**
135+
* Writes a timestamp value as a JSON key.
136+
*
137+
* @param epochSecond the epoch second of the timestamp to write
138+
* @param nano the nanoseconds of the timestamp to write
139+
* @throws JsonWriterException if the nanoseconds value is less than 0 or greater than 999999999
140+
*/
141+
def writeTimestampKey(epochSecond: Long, nano: Int): Unit = {
142+
if (nano < 0 || nano > 999999999) encodeError("illegal nanoseconds value: " + nano)
143+
writeOptionalCommaAndIndentionBeforeKey()
144+
writeBytes('"')
145+
var pos = ensureBufCapacity(30)
146+
val buf = this.buf
147+
var es = epochSecond
148+
var ns = nano
149+
if (es < 0 & ns > 0) {
150+
es += 1
151+
ns = 1000000000 - ns
152+
if (es == 0) {
153+
buf(pos) = '-'
154+
pos += 1
155+
}
156+
}
157+
pos = writeLong(es, pos, buf)
158+
if (ns != 0) {
159+
val dotPos = pos
160+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
161+
buf(dotPos) = '.'
162+
}
163+
this.count = pos
164+
writeParenthesesWithColon()
165+
}
166+
134167
/**
135168
* Writes a `BigInt` value as a JSON key.
136169
*
@@ -709,10 +742,20 @@ final class JsonWriter private[jsoniter_scala](
709742
writeOptionalCommaAndIndentionBeforeValue()
710743
var pos = ensureBufCapacity(30)
711744
val buf = this.buf
712-
pos = writeLong(epochSecond, pos, buf)
713-
if (nano != 0) {
745+
var es = epochSecond
746+
var ns = nano
747+
if (es < 0 & ns > 0) {
748+
es += 1
749+
ns = 1000000000 - ns
750+
if (es == 0) {
751+
buf(pos) = '-'
752+
pos += 1
753+
}
754+
}
755+
pos = writeLong(es, pos, buf)
756+
if (ns != 0) {
714757
val dotPos = pos
715-
pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits)
758+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
716759
buf(dotPos) = '.'
717760
}
718761
this.count = pos
@@ -843,10 +886,20 @@ final class JsonWriter private[jsoniter_scala](
843886
val buf = this.buf
844887
buf(pos) = '"'
845888
pos += 1
846-
pos = writeLong(epochSecond, pos, buf)
847-
if (nano != 0) {
889+
var es = epochSecond
890+
var ns = nano
891+
if (es < 0 & ns > 0) {
892+
es += 1
893+
ns = 1000000000 - ns
894+
if (es == 0) {
895+
buf(pos) = '-'
896+
pos += 1
897+
}
898+
}
899+
pos = writeLong(es, pos, buf)
900+
if (ns != 0) {
848901
val dotPos = pos
849-
pos = writeSignificantFractionDigits(nano, pos + 9, pos, buf, digits)
902+
pos = writeSignificantFractionDigits(ns, pos + 9, pos, buf, digits)
850903
buf(dotPos) = '.'
851904
}
852905
buf(pos) = '"'

jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriterSpec.scala

+13-10
Original file line numberDiff line numberDiff line change
@@ -737,18 +737,21 @@ class JsonWriterSpec extends AnyWordSpec with Matchers with ScalaCheckPropertyCh
737737
"JsonWriter.writeVal for a timestamp" should {
738738
"write timestamp values" in {
739739
def check(epochSecond: Long, nano: Int): Unit = {
740-
val s = BigDecimal({
741-
val es = java.math.BigDecimal.valueOf(epochSecond)
742-
if (nano == 0) es
743-
else es.add(java.math.BigDecimal.valueOf({
744-
if (epochSecond < 0) -nano
745-
else nano
746-
}.toLong, 9).stripTrailingZeros)
747-
}).toString
748-
withWriter(_.writeTimestampVal(epochSecond, nano)) shouldBe s
749-
withWriter(_.writeTimestampValAsString(epochSecond, nano)) shouldBe s""""$s""""
740+
val s =
741+
if (nano == 0) epochSecond.toString
742+
else BigDecimal({
743+
java.math.BigDecimal.valueOf(epochSecond)
744+
.add(java.math.BigDecimal.valueOf(nano.toLong, 9).stripTrailingZeros)
745+
}).toString
746+
if (!s.contains("E")) {
747+
withWriter(_.writeTimestampVal(epochSecond, nano)) shouldBe s
748+
withWriter(_.writeTimestampValAsString(epochSecond, nano)) shouldBe s""""$s""""
749+
withWriter(_.writeTimestampKey(epochSecond, nano)) shouldBe s""""$s":"""
750+
}
750751
}
751752

753+
check(-1L, 123456789)
754+
check(-1L, 0)
752755
check(1L, 0)
753756
check(1L, 900000000)
754757
check(1L, 990000000)

version.sbt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ThisBuild / version := "2.32.1-SNAPSHOT"
1+
ThisBuild / version := "2.33.0-SNAPSHOT"

0 commit comments

Comments
 (0)