diff --git a/modules/core/src-js/smithy4s/Timestamp.scala b/modules/core/src-js/smithy4s/Timestamp.scala index f59d5e6e0..fbfec6f8a 100644 --- a/modules/core/src-js/smithy4s/Timestamp.scala +++ b/modules/core/src-js/smithy4s/Timestamp.scala @@ -81,25 +81,25 @@ case class Timestamp private (epochSecond: Long, nano: Int) { append2Digits(day, s.append(' ')) s.append(' ').append(months(month - 1)) append4Digits(year, s.append(' ')) - appendTime(secsOfDay, s.append(' ')) + appendTime(secsOfDay, s.append(' '), addSeparator = true) appendNano(nano, s) s.append(" GMT").toString case 2 => append4Digits(year, s) - append2Digits(month, s.append('-')) - append2Digits(day, s.append('-')) + append2Digits(month, s) + append2Digits(day, s) s.toString case 3 => append4Digits(year, s) - append2Digits(month, s.append('-')) - append2Digits(day, s.append('-')) - appendTime(secsOfDay, s.append('T')) + append2Digits(month, s) + append2Digits(day, s) + appendTime(secsOfDay, s.append('T'), addSeparator = false) s.append('Z').toString case _ => append4Digits(year, s) append2Digits(month, s.append('-')) append2Digits(day, s.append('-')) - appendTime(secsOfDay, s.append('T')) + appendTime(secsOfDay, s.append('T'), addSeparator = true) appendNano(nano, s) s.append('Z').toString } @@ -114,15 +114,23 @@ case class Timestamp private (epochSecond: Long, nano: Int) { private[this] def appendTime( secsOfDay: Int, - s: java.lang.StringBuilder + s: java.lang.StringBuilder, + addSeparator: Boolean ): Unit = { val minutesOfDay = secsOfDay / 60 val hour = minutesOfDay / 60 val minute = minutesOfDay - hour * 60 val second = secsOfDay - minutesOfDay * 60 - append2Digits(hour, s) - append2Digits(minute, s.append(':')) - append2Digits(second, s.append(':')) + + if (addSeparator) { + append2Digits(hour, s) + append2Digits(minute, s.append(':')) + append2Digits(second, s.append(':')) + } else { + append2Digits(hour, s) + append2Digits(minute, s) + append2Digits(second, s) + } } private[this] def appendNano(nano: Int, s: java.lang.StringBuilder): Unit = diff --git a/modules/core/src-jvm-native/smithy4s/Timestamp.scala b/modules/core/src-jvm-native/smithy4s/Timestamp.scala index d2890c098..dff0b9040 100644 --- a/modules/core/src-jvm-native/smithy4s/Timestamp.scala +++ b/modules/core/src-jvm-native/smithy4s/Timestamp.scala @@ -73,25 +73,25 @@ case class Timestamp private (epochSecond: Long, nano: Int) append2Digits(day, s.append(' ')) s.append(' ').append(Timestamp.months(month - 1)) append4Digits(year, s.append(' ')) - appendTime(secsOfDay, s.append(' ')) + appendTime(secsOfDay, s.append(' '), addSeparator = true) appendNano(nano, s) s.append(" GMT").toString case 2 => append4Digits(year, s) - append2Digits(month, s.append('-')) - append2Digits(day, s.append('-')) + append2Digits(month, s) + append2Digits(day, s) s.toString case 3 => append4Digits(year, s) - append2Digits(month, s.append('-')) - append2Digits(day, s.append('-')) - appendTime(secsOfDay, s.append('T')) + append2Digits(month, s) + append2Digits(day, s) + appendTime(secsOfDay, s.append('T'), addSeparator = false) s.append('Z').toString case _ => append4Digits(year, s) append2Digits(month, s.append('-')) append2Digits(day, s.append('-')) - appendTime(secsOfDay, s.append('T')) + appendTime(secsOfDay, s.append('T'), addSeparator = true) appendNano(nano, s) s.append('Z').toString } @@ -106,15 +106,23 @@ case class Timestamp private (epochSecond: Long, nano: Int) private[this] def appendTime( secsOfDay: Int, - s: java.lang.StringBuilder + s: java.lang.StringBuilder, + addSeparator: Boolean ): Unit = { val y1 = secsOfDay * 1193047L // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ val y2 = (y1 & 0xffffffffL) * 60 val y3 = (y2 & 0xffffffffL) * 60 - append2Digits((y1 >> 32).toInt, s) - append2Digits((y2 >> 32).toInt, s.append(':')) - append2Digits((y3 >> 32).toInt, s.append(':')) + + if (addSeparator) { + append2Digits((y1 >> 32).toInt, s) + append2Digits((y2 >> 32).toInt, s.append(':')) + append2Digits((y3 >> 32).toInt, s.append(':')) + } else { + append2Digits((y1 >> 32).toInt, s) + append2Digits((y2 >> 32).toInt, s) + append2Digits((y3 >> 32).toInt, s) + } } private[this] def appendNano(nano: Int, s: java.lang.StringBuilder): Unit = diff --git a/modules/core/test/src-js/smithy4s/TimestampSpec.scala b/modules/core/test/src-js/smithy4s/TimestampSpec.scala index 314f592c8..64a832e29 100644 --- a/modules/core/test/src-js/smithy4s/TimestampSpec.scala +++ b/modules/core/test/src-js/smithy4s/TimestampSpec.scala @@ -127,4 +127,36 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite { expect.same(parsed, None) } } + + property("Converts to concise date format") { + forAll { (i: Date) => + val epochSecond = (i.valueOf() / 1000).toLong + val nano = (i.valueOf() % 1000).toInt * 1000000 + val ts = Timestamp(epochSecond, nano) + val formatted = ts.conciseDate + val year = i.getUTCFullYear().toInt + val month = i.getUTCMonth().toInt + 1 // in js month is 0-11 + val date = i.getUTCDate().toInt + val expected = f"$year%04d$month%02d$date%02d" + expect.same(formatted, expected) + } + } + + property("Converts to concise date time format") { + forAll { (i: Date) => + val epochSecond = (i.valueOf() / 1000).toLong + val nano = (i.valueOf() % 1000).toInt * 1000000 + val ts = Timestamp(epochSecond = epochSecond, nano = nano) + val formatted = ts.conciseDateTime + val year = i.getUTCFullYear().toInt + val month = i.getUTCMonth().toInt + 1 // in js month is 0-11 + val date = i.getUTCDate().toInt + val hours = i.getUTCHours().toInt + val minutes = i.getUTCMinutes().toInt + val seconds = i.getUTCSeconds().toInt + val expected = + f"$year%04d$month%02d$date%02dT$hours%02d$minutes%02d$seconds%02dZ" + expect.same(formatted, expected) + } + } } diff --git a/modules/core/test/src-jvm/smithy4s/TimestampSpec.scala b/modules/core/test/src-jvm/smithy4s/TimestampSpec.scala index d39ca6a00..c784039fe 100644 --- a/modules/core/test/src-jvm/smithy4s/TimestampSpec.scala +++ b/modules/core/test/src-jvm/smithy4s/TimestampSpec.scala @@ -138,4 +138,30 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite { expect.same(parsed, None) } } + + private val conciseDateFormatter = DateTimeFormatter + .ofPattern("yyyyMMdd", Locale.ENGLISH) + .withZone(ZoneOffset.UTC) + + private val conciseDateTimeFormatter = DateTimeFormatter + .ofPattern("yyyyMMdd'T'HHmmssX", Locale.ENGLISH) + .withZone(ZoneOffset.UTC) + + property("Converts to concise date format") { + forAll { (i: Instant) => + val ts = Timestamp(i.getEpochSecond, i.getNano) + val formatted = ts.conciseDate + val expected = conciseDateFormatter.format(i) + expect.same(formatted, expected) + } + } + + property("Converts to concise date time format") { + forAll { (i: Instant) => + val ts = Timestamp(i.getEpochSecond, i.getNano) + val formatted = ts.conciseDateTime + val expected = conciseDateTimeFormatter.format(i) + expect.same(formatted, expected) + } + } }