Skip to content

Commit

Permalink
Merge pull request #600 from commercetools/format-millis
Browse files Browse the repository at this point in the history
Always render milliseconds for instant and time
  • Loading branch information
satabin authored Jul 31, 2024
2 parents 10a1461 + 13f9628 commit 5e1a40f
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 5e1a40f

Please sign in to comment.