Skip to content

Commit

Permalink
Always render milliseconds for instant and time
Browse files Browse the repository at this point in the history
Checks that rendering date time and instant gives the same result, and
that LocalTime also renders in the same way for both representations.
  • Loading branch information
satabin committed Jul 31, 2024
1 parent 501224d commit 13f9628
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 15 deletions.
4 changes: 2 additions & 2 deletions json/json-core/src/main/scala/io/sphere/json/FromJSON.scala
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ object FromJSON extends FromJSONInstances {
.appendPattern("'T'[HH[:mm[:ss]]]")
.appendFraction(time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true)
.optionalStart()
.appendZoneOrOffsetId()
.appendOffset("+HHmm", "Z")
.optionalEnd()
.optionalEnd()
.parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L)
Expand All @@ -400,8 +400,8 @@ object FromJSON extends FromJSONInstances {
.parseDefaulting(time.temporal.ChronoField.MINUTE_OF_HOUR, 0L)
.parseDefaulting(time.temporal.ChronoField.SECOND_OF_MINUTE, 0L)
.parseDefaulting(time.temporal.ChronoField.NANO_OF_SECOND, 0L)
.parseDefaulting(time.temporal.ChronoField.OFFSET_SECONDS, 0L)
.toFormatter()
.withZone(time.ZoneOffset.UTC)

implicit val javaInstantReader: FromJSON[time.Instant] =
jsonStringReader("Failed to parse date/time: %s")(s =>
Expand Down
14 changes: 11 additions & 3 deletions json/json-core/src/main/scala/io/sphere/json/ToJSON.scala
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,22 @@ object ToJSON extends ToJSONInstances {
}

// java.time

// always format the milliseconds
private val javaInstantFormatter = new time.format.DateTimeFormatterBuilder()
.appendInstant(3)
.toFormatter()
implicit val javaInstantWriter: ToJSON[time.Instant] = new ToJSON[time.Instant] {
def write(value: time.Instant): JValue = JString(
time.format.DateTimeFormatter.ISO_INSTANT.format(value))
javaInstantFormatter.format(time.OffsetDateTime.ofInstant(value, time.ZoneOffset.UTC)))
}

// always format the milliseconds
private val javaLocalTimeFormatter = new time.format.DateTimeFormatterBuilder()
.appendPattern("HH:mm:ss.SSS")
.toFormatter()
implicit val javaTimeWriter: ToJSON[time.LocalTime] = new ToJSON[time.LocalTime] {
def write(value: time.LocalTime): JValue = JString(
time.format.DateTimeFormatter.ISO_LOCAL_TIME.format(value))
def write(value: time.LocalTime): JValue = JString(javaLocalTimeFormatter.format(value))
}

implicit val javaDateWriter: ToJSON[time.LocalDate] = new ToJSON[time.LocalDate] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,52 +114,52 @@ class DateTimeParsingSpec extends AnyWordSpec with Matchers {
// ensures that we accept similar patterns as joda when parsing instants
"parsing a Java instant" should {
"accept a full instant with milliseconds and offset" in {
javaInstantReader.read(JString("2004-06-09T12:24:48.501+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T12:24:48.501+0800")) shouldBe Valid(
Instant.parse("2004-06-09T04:24:48.501Z"))
}

"accept a year with offset" in {
javaInstantReader.read(JString("2004T+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004T+0800")) shouldBe Valid(
Instant.parse("2004-01-01T00:00:00+08:00"))
}

"accept a year month with offset" in {
javaInstantReader.read(JString("2004-06T+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06T+0800")) shouldBe Valid(
Instant.parse("2004-06-01T00:00:00+08:00"))
}

"accept a year month day with offset" in {
javaInstantReader.read(JString("2004-06-09T+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T+0800")) shouldBe Valid(
Instant.parse("2004-06-09T00:00:00+08:00"))
}

"accept a year month day with hour and offset" in {
javaInstantReader.read(JString("2004-06-09T12+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T12+0800")) shouldBe Valid(
Instant.parse("2004-06-09T04:00:00Z"))
}

"accept a year month day with hour, minute, and offset" in {
javaInstantReader.read(JString("2004-06-09T12:24+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T12:24+0800")) shouldBe Valid(
Instant.parse("2004-06-09T04:24:00Z"))
}

"accept a year month day with hour, minute, second, and offset" in {
javaInstantReader.read(JString("2004-06-09T12:24:48+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T12:24:48+0800")) shouldBe Valid(
Instant.parse("2004-06-09T04:24:48Z"))
}

"accept a year month day with hour, fraction, and offset" in {
javaInstantReader.read(JString("2004-06-09T12.5+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T12.5+0800")) shouldBe Valid(
Instant.parse("2004-06-09T04:00:00.5Z"))
}

"accept a year month day with hour, minute, fraction, and offset" in {
javaInstantReader.read(JString("2004-06-09T12:24.5+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T12:24.5+0800")) shouldBe Valid(
Instant.parse("2004-06-09T04:24:00.5Z"))
}

"accept a year month day with hour, minute, second, fraction, and offset" in {
javaInstantReader.read(JString("2004-06-09T12:24:48.5+08:00")) shouldBe Valid(
javaInstantReader.read(JString("2004-06-09T12:24:48.5+0800")) shouldBe Valid(
Instant.parse("2004-06-09T04:24:48.5Z"))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ object JSONProperties extends Properties("JSON") {
ms <- Gen.choose(0, 999)
} yield new DateTime(y, m, d, h, min, s, ms, DateTimeZone.UTC))

// generate dates between years -4000 and +4000
implicit val javaInstant: Arbitrary[time.Instant] =
Arbitrary(Gen.choose(-188395027761000L, 64092207599999L).map(time.Instant.ofEpochMilli(_)))

implicit val javaLocalTime: Arbitrary[time.LocalTime] = Arbitrary(
Gen.choose(0, 3600 * 24).map(time.LocalTime.ofSecondOfDay(_)))

implicit def arbitraryDate: Arbitrary[LocalDate] =
Arbitrary(Arbitrary.arbitrary[DateTime].map(_.toLocalDate))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.sphere.json

import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.LocalDate
import org.joda.time.LocalTime
import org.joda.time.YearMonth
import org.scalacheck.Arbitrary
import org.scalacheck.Gen
import org.scalacheck.Prop
import org.scalacheck.Properties
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

import java.time.{Instant => JInstant}
import java.time.{LocalDate => JLocalDate}
import java.time.{LocalTime => JLocalTime}
import java.time.{YearMonth => JYearMonth}
import cats.data.Validated

class JodaJavaTimeCompat extends Properties("Joda - java.time compat") {
val epochMillis = Gen.choose(-188395027761000L, 64092207599999L)

implicit def arbitraryDateTime: Arbitrary[DateTime] =
Arbitrary(epochMillis.map(new DateTime(_, DateTimeZone.UTC)))

// generate dates between years -4000 and +4000
implicit val javaInstant: Arbitrary[JInstant] =
Arbitrary(epochMillis.map(JInstant.ofEpochMilli(_)))

implicit val javaLocalTime: Arbitrary[JLocalTime] = Arbitrary(
Gen.choose(0, 3600 * 24).map(JLocalTime.ofSecondOfDay(_)))

property("compatibility between serialized Instant and DateTime") = Prop.forAll {
(instant: JInstant) =>
val dateTime = new DateTime(instant.toEpochMilli(), DateTimeZone.UTC)
val serializedInstant = ToJSON[JInstant].write(instant)
val serializedDateTime = ToJSON[DateTime].write(dateTime)
serializedInstant == serializedDateTime
}

property("compatibility between serialized java.time.LocalTime and org.joda.time.LocalTime") =
Prop.forAll { (javaTime: JLocalTime) =>
val jodaTime = LocalTime.fromMillisOfDay(javaTime.toNanoOfDay() / 1000000)
val serializedJavaTime = ToJSON[JLocalTime].write(javaTime)
val serializedJodaTime = ToJSON[LocalTime].write(jodaTime)
serializedJavaTime == serializedJodaTime
}

property("roundtrip from java.time.Instant") = Prop.forAll { (instant: JInstant) =>
FromJSON[DateTime]
.read(ToJSON[JInstant].write(instant))
.andThen { dateTime =>
FromJSON[JInstant].read(ToJSON[DateTime].write(dateTime))
}
.fold(_ => false, _ == instant)
}

property("roundtrip from org.joda.time.DateTime") = Prop.forAll { (dateTime: DateTime) =>
FromJSON[JInstant]
.read(ToJSON[DateTime].write(dateTime))
.andThen { instant =>
FromJSON[DateTime].read(ToJSON[JInstant].write(instant))
}
.fold(_ => false, _ == dateTime)
}

}

0 comments on commit 13f9628

Please sign in to comment.