zio/zio-json

Better ISO-8601 support

paulpdaniels opened this issue · 1 comments

According to the section on timezone designators: https://en.wikipedia.org/wiki/ISO_8601

The following should be valid:

+hh
+hhmm
+hh:mm

However only (1) and (3) are actually supported. I tried the base java.time and circe and they had the same failure cases, so it's at least consistent but I don't think it is fully correct.

In fact, zio-json implements none of them, because currently supported format is Z or +hh:mm[:ss] or -hh:mm[:ss].

So it will not reject seconds in time-zone values, and that is how the default parser for time-zones works in Java and corresponding implementations for Scala.js and Scala Native:

scala> import java.time.*
                                                                                                                                                                                                                                                                                                                                                                               
scala> import java.time.format.*
                                                                                                                                                                                        
scala> val fmtXXX = DateTimeFormatter.ofPattern("XXX")
val fmtXXX: java.time.format.DateTimeFormatter = Offset(+HH:MM,'Z')
                                                                                                                                                                                        
scala> val fmtX = DateTimeFormatter.ofPattern("X")
val fmtX: java.time.format.DateTimeFormatter = Offset(+HHmm,'Z')
                                                                                                                                                                                        
scala> val fmtZ = DateTimeFormatter.ofPattern("Z")
val fmtZ: java.time.format.DateTimeFormatter = Offset(+HHMM,'+0000')
                                                                                                                                                                                        
scala> val tz = ZoneOffset.ofHoursMinutesSeconds(12, 34, 56)
val tz: java.time.ZoneOffset = +12:34:56
                                                                                                                                                                                        
scala> tz.toString
val res0: String = +12:34:56
                                                                                                                                                                                        
scala> fmtXXX.format(tz)
val res1: String = +12:34
                                                                                                                                                                                        
scala> fmtX.format(tz)
val res2: String = +1234
                                                                                                                                                                                        
scala> fmtZ.format(tz)
val res3: String = +1234
                                                                                                                                                                                        
scala> ZoneOffset.of("+12:34:56")
val res4: java.time.ZoneOffset = +12:34:56
                                                                                                                                                                                        
scala> fmtXXX.parse("+12:34:56")
java.time.format.DateTimeParseException: Text '+12:34:56' could not be parsed, unparsed text found at index 6
  at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2055)
  at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1880)
  ... 32 elided
                                                                                                                                                                                        
scala> fmtX.parse("+12:34:56")
java.time.format.DateTimeParseException: Text '+12:34:56' could not be parsed, unparsed text found at index 3
  at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2055)
  at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1880)
  ... 32 elided
                                                                                                                                                                                        
scala> fmtZ.parse("+12:34:56")
java.time.format.DateTimeParseException: Text '+12:34:56' could not be parsed at index 0
  at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2052)
  at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1880)
  ... 32 elided

But you can override default decoders/encoders for any type (including java.time.* types), and try to parse by fmtX and fmtXXX formatters sequentially in case of error:

scala> def iso8601_timezone_parser(s: String): Try[ZoneOffset] = Try(fmtX.parse(s)).orElse(Try(fmtXXX.parse(s))).flatMap(x => Try(ZoneOffset.from(x))).filter(_ => s != "Z")
def iso8601_timezone_parser(s: String): util.Try[java.time.ZoneOffset]

scala> iso8601_timezone_parser("Z")
val res5: util.Try[java.time.ZoneOffset] = Failure(java.util.NoSuchElementException: Predicate does not hold for Z)
                                                                                                                                                                                        
scala> iso8601_timezone_parser("+12")
val res6: util.Try[java.time.ZoneOffset] = Success(+12:00)
                                                                                                                                                                                        
scala> iso8601_timezone_parser("+12:34")
val res7: util.Try[java.time.ZoneOffset] = Success(+12:34)
                                                                                                                                                                                        
scala> iso8601_timezone_parser("+1234")
val res8: util.Try[java.time.ZoneOffset] = Success(+12:34)
                                                                                                                                                                                        
scala> iso8601_timezone_parser("+123456")
val res9: util.Try[java.time.ZoneOffset] = Failure(java.time.format.DateTimeParseException: Text '+123456' could not be parsed at index 0)
                                                                                                                                                                                        
scala> iso8601_timezone_parser("+12:34:56")
val res10: util.Try[java.time.ZoneOffset] = Failure(java.time.format.DateTimeParseException: Text '+12:34:56' could not be parsed, unparsed text found at index 6)