Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parsing of time zones for DATE_TIME format of Timestamp #844

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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