Skip to content

Commit

Permalink
Merge pull request #844 from plokhotnyuk/add-parsing-of-timezones-for…
Browse files Browse the repository at this point in the history
…-datetime-format

Add parsing of time zones for `DATE_TIME` format of `Timestamp`
  • Loading branch information
Baccata authored Mar 2, 2023
2 parents 0d6ad41 + 1770f99 commit 6811bac
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 32 deletions.
69 changes: 55 additions & 14 deletions modules/core/src-js/smithy4s/Timestamp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package smithy4s

import smithy.api.TimestampFormat
import smithy4s.Timestamp._
import scalajs.js.Date
import scala.util.control.{NoStackTrace, NonFatal}

Expand Down Expand Up @@ -77,9 +76,10 @@ case class Timestamp private (epochSecond: Long, nano: Int) {
marchDayOfYear - ((marchMonth * 1002762 - 16383) >> 15) // marchDayOfYear - (marchMonth * 306 + 5) / 10 + 1
internalFormat match {
case 1 =>
s.append(daysOfWeek(((epochDay + 700000003) % 7).toInt)).append(',')
s.append(Timestamp.daysOfWeek(((epochDay + 700000003) % 7).toInt))
.append(',')
append2Digits(day, s.append(' '))
s.append(' ').append(months(month - 1))
s.append(' ').append(Timestamp.months(month - 1))
append4Digits(year, s.append(' '))
appendTime(secsOfDay, s.append(' '), addSeparator = true)
appendNano(nano, s)
Expand Down Expand Up @@ -141,7 +141,7 @@ case class Timestamp private (epochSecond: Long, nano: Int) {
append2Digits(q1, s)
val q2 = r1 / 100000
val r2 = r1 - q2 * 100000
val d = digits(q2)
val d = Timestamp.digits(q2)
s.append(d.toByte.toChar)
if (r2 != 0 || d > 0x3039) { // check if nano is divisible by 1000000
s.append((d >> 8).toByte.toChar)
Expand All @@ -167,7 +167,7 @@ case class Timestamp private (epochSecond: Long, nano: Int) {
}

private[this] def append2Digits(x: Int, s: java.lang.StringBuilder): Unit = {
val d = digits(x)
val d = Timestamp.digits(x)
val _ = s.append((d & 0xff).toChar).append((d >> 8).toChar)
}

Expand Down Expand Up @@ -352,6 +352,11 @@ object Timestamp {
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
}
var epochSecond = toEpochDay(
year,
month,
day
) * 86400 + (hour * 3600 + minute * 60 + second)
var nano = 0
var ch = (0: Char)
if (pos < len) {
Expand All @@ -372,15 +377,51 @@ object Timestamp {
}
}
}
if (ch != 'Z' || pos != len) error()
new Timestamp(
toEpochDay(
year,
month,
day
) * 86400 + (hour * 3600 + minute * 60 + second),
nano
)
if (ch != 'Z') {
val isNeg = ch == '-' || (ch != '+' && {
error()
true
})
if (pos + 2 > len) error()
var offsetTotal = {
val ch0 = s.charAt(pos)
val ch1 = s.charAt(pos + 1)
if (ch0 < '0' || ch0 > '1' || ch1 < '0' || ch1 > '9') error()
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
} * 3600
if (
pos + 3 <= len && {
ch = s.charAt(pos)
pos += 1
ch == ':'
} && {
offsetTotal += {
val ch0 = s.charAt(pos)
val ch1 = s.charAt(pos + 1)
if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
} * 60
pos + 3 <= len
} && {
ch = s.charAt(pos)
pos += 1
ch == ':'
}
) offsetTotal += {
val ch0 = s.charAt(pos)
val ch1 = s.charAt(pos + 1)
if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
}
if (offsetTotal > 64800) error() // 64800 == 18 * 60 * 60
if (isNeg) offsetTotal = -offsetTotal
epochSecond -= offsetTotal
}
if (pos != len) error()
new Timestamp(epochSecond, nano)
}

private[this] def parseEpochSeconds(s: String): Timestamp = {
Expand Down
59 changes: 50 additions & 9 deletions modules/core/src-jvm-native/smithy4s/Timestamp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,11 @@ object Timestamp extends TimestampCompanionPlatform {
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
}
var epochSecond = toEpochDay(
year,
month,
day
) * 86400 + (hour * 3600 + minute * 60 + second)
var nano = 0
var ch = (0: Char)
if (pos < len) {
Expand All @@ -349,15 +354,51 @@ object Timestamp extends TimestampCompanionPlatform {
}
}
}
if (ch != 'Z' || pos != len) error()
new Timestamp(
toEpochDay(
year,
month,
day
) * 86400 + (hour * 3600 + minute * 60 + second),
nano
)
if (ch != 'Z') {
val isNeg = ch == '-' || (ch != '+' && {
error()
true
})
if (pos + 2 > len) error()
var offsetTotal = {
val ch0 = s.charAt(pos)
val ch1 = s.charAt(pos + 1)
if (ch0 < '0' || ch0 > '1' || ch1 < '0' || ch1 > '9') error()
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
} * 3600
if (
pos + 3 <= len && {
ch = s.charAt(pos)
pos += 1
ch == ':'
} && {
offsetTotal += {
val ch0 = s.charAt(pos)
val ch1 = s.charAt(pos + 1)
if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
} * 60
pos + 3 <= len
} && {
ch = s.charAt(pos)
pos += 1
ch == ':'
}
) offsetTotal += {
val ch0 = s.charAt(pos)
val ch1 = s.charAt(pos + 1)
if (ch0 < '0' || ch0 > '5' || ch1 < '0' || ch1 > '9') error()
pos += 2
ch0 * 10 + ch1 - 528 // 528 == '0' * 11
}
if (offsetTotal > 64800) error() // 64800 == 18 * 60 * 60
if (isNeg) offsetTotal = -offsetTotal
epochSecond -= offsetTotal
}
if (pos != len) error()
new Timestamp(epochSecond, nano)
}

private[this] def parseEpochSeconds(s: String): Timestamp = {
Expand Down
38 changes: 31 additions & 7 deletions modules/core/test/src-js/smithy4s/TimestampSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,34 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite {
}
}

property("Converts from DATE_TIME format with timezone offset") {
forAll { (i: Date, o: Int) =>
val totalOffset = Math.abs(o % 64800)
val offsetHours = totalOffset / 3600
val offsetMinutes = (totalOffset % 3600) / 60
val offsetSeconds = totalOffset % 60
val epochSecond = (i.valueOf() / 1000).toLong
val nano = (i.valueOf() % 1000).toInt * 1000000
val formatted = Timestamp(epochSecond, nano)
.format(TimestampFormat.DATE_TIME)
.dropRight(1) + {
if (offsetSeconds == 0)
f"${if (o >= 0) "+" else "-"}$offsetHours%02d:$offsetMinutes%02d"
else
f"${if (o >= 0) "+" else "-"}$offsetHours%02d:$offsetMinutes%02d:$offsetSeconds%02d"
}
val ts = Timestamp(
epochSecond + {
if (o >= 0) -totalOffset
else totalOffset
},
nano
)
val parsed = Timestamp.parse(formatted, TimestampFormat.DATE_TIME)
expect.same(parsed, Some(ts))
}
}

property("Converts to/from HTTP_DATE format") {
forAll { (i: Date) =>
val epochSecond = (i.valueOf() / 1000).toLong
Expand Down Expand Up @@ -110,13 +138,9 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite {
}

property("Parse EPOCH_SECONDS format with invalid input") {
val EpochFormat = """^(\d+)(\.(\d+))?""".r
forAll { (str: String) =>
val parsed = Timestamp.parse(str, TimestampFormat.EPOCH_SECONDS)
parsed match {
case Some(_) => expect(EpochFormat.pattern.matcher(str).matches)
case None => expect(!EpochFormat.pattern.matcher(str).matches)
}
forAll(Gen.alphaStr) { (str: String) =>
val parsed = Timestamp.parse(str + "X", TimestampFormat.EPOCH_SECONDS)
expect.same(parsed, None)
}
}

Expand Down
30 changes: 28 additions & 2 deletions modules/core/test/src-jvm/smithy4s/TimestampSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite {
}
}

property("Converts from DATE_TIME format with timezone offset") {
forAll { (i: Instant, o: Int) =>
val totalOffset = Math.abs(o % 64800)
val offsetHours = totalOffset / 3600
val offsetMinutes = (totalOffset % 3600) / 60
val offsetSeconds = totalOffset % 60
val formatted = Timestamp(i.getEpochSecond, i.getNano)
.format(TimestampFormat.DATE_TIME)
.dropRight(1) + {
if (offsetSeconds == 0)
f"${if (o >= 0) "+" else "-"}$offsetHours%02d:$offsetMinutes%02d"
else
f"${if (o >= 0) "+" else "-"}$offsetHours%02d:$offsetMinutes%02d:$offsetSeconds%02d"
}
val ts = Timestamp(
i.getEpochSecond + {
if (o >= 0) -totalOffset
else totalOffset
},
i.getNano
)
val parsed = Timestamp.parse(formatted, TimestampFormat.DATE_TIME)
expect.same(parsed, Some(ts))
}
}

property("Converts to/from HTTP_DATE format") {
forAll { (i: Instant) =>
val ts = Timestamp(i.getEpochSecond, i.getNano)
Expand Down Expand Up @@ -122,8 +148,8 @@ class TimestampSpec() extends munit.FunSuite with munit.ScalaCheckSuite {

property("Parse EPOCH_SECONDS format with invalid input") {
forAll(Gen.alphaStr) { (str: String) =>
val parsed = Timestamp.parse(str, TimestampFormat.EPOCH_SECONDS)
expect(parsed.isEmpty)
val parsed = Timestamp.parse(str + "X", TimestampFormat.EPOCH_SECONDS)
expect.same(parsed, None)
}
}

Expand Down

0 comments on commit 6811bac

Please sign in to comment.