All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.rojoma.json.v3.util.time.ISO8601.scala Maven / Gradle / Ivy

The newest version!
package com.rojoma.json.v3.util.time

import java.util.regex.Pattern
import java.util.Date
import java.time._
import java.time.format.{DateTimeFormatter, DateTimeParseException}

import com.rojoma.json.v3.ast._
import com.rojoma.json.v3.codec._

object ISO8601 {
  object codec {
    // technically this is mixing basic and extended timestamps, which
    // is not ISO, but it's common enough that I'll allow it.
    private val offsetDateTimeFixupPattern = Pattern.compile("^(.*[tT].*[+-])([0-9]{2}(?:[0-9]{2})?)$")

    private def fixupOffset(pattern: Pattern, s: String): String = {
      val m = pattern.matcher(s)
      if(m.matches) {
        val base = m.group(1)
        val tz = m.group(2)
        if(tz.length == 4) {
          s"$base${tz.substring(0,2)}:${tz.substring(2)}"
        } else {
          s"$base$tz:00"
        }
      } else {
        s
      }
    }

    implicit object dateCodec extends JsonEncode[Date] with JsonDecode[Date] {
      def encode(x: Date) = instantCodec.encode(x.toInstant)

      def decode(x: JValue) = instantCodec.decode(x).right.flatMap { instant =>
        try {
          Right(new Date(instant.toEpochMilli))
        } catch {
          case e: ArithmeticException =>
            Left(DecodeError.InvalidValue(x))
        }
      }
    }

    implicit object instantCodec extends JsonEncode[Instant] with JsonDecode[Instant] {
      private val fixupPattern = offsetDateTimeFixupPattern

      def encode(x: Instant) =
        JString(DateTimeFormatter.ISO_INSTANT.format(x))

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(ParseHelper.parseInstant(fixupOffset(fixupPattern, s)))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object offsetDateTimeCodec extends JsonEncode[OffsetDateTime] with JsonDecode[OffsetDateTime] {
      private val fixupPattern = offsetDateTimeFixupPattern

      def encode(x: OffsetDateTime) =
        JString(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(x))

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(ParseHelper.parseOffsetDateTime(fixupOffset(fixupPattern, s)))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object offsetTimeCodec extends JsonEncode[OffsetTime] with JsonDecode[OffsetTime] {
      def encode(x: OffsetTime) =
        JString(DateTimeFormatter.ISO_OFFSET_TIME.format(x))

      private val fixupPattern = Pattern.compile("^(.*[+-])([0-9]{2}(?:[0-9]{2})?)$")

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(ParseHelper.parseOffsetTime(fixupOffset(fixupPattern, s)))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object zoneOffsetCodec extends JsonEncode[ZoneOffset] with JsonDecode[ZoneOffset] {
      def encode(x: ZoneOffset) = JString(x.toString)

      private val fixupPattern = Pattern.compile("^([+-])([0-9]{2}(?:[0-9]{2})?)$")

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(ZoneOffset.of(fixupOffset(fixupPattern, s)))
            } catch {
              case e: DateTimeException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object localDateTimeCodec extends JsonEncode[LocalDateTime] with JsonDecode[LocalDateTime] {
      def encode(x: LocalDateTime) =
        JString(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(x))

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(ParseHelper.parseLocalDateTime(s))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object localDateCodec extends JsonEncode[LocalDate] with JsonDecode[LocalDate] {
      def encode(x: LocalDate) =
        JString(DateTimeFormatter.ISO_LOCAL_DATE.format(x))

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(ParseHelper.parseLocalDate(s))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object localTimeCodec extends JsonEncode[LocalTime] with JsonDecode[LocalTime] {
      def encode(x: LocalTime) =
        JString(DateTimeFormatter.ISO_LOCAL_TIME.format(x))

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(ParseHelper.parseLocalTime(s))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object yearMonthCodec extends JsonEncode[YearMonth] with JsonDecode[YearMonth] {
      def encode(x: YearMonth) = {
        val s = x.toString
        if(x.getYear > 9999 && s.charAt(0) != '+') JString("+" + s)
        else JString(s)
      }

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(YearMonth.parse(s))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object monthDayCodec extends JsonEncode[MonthDay] with JsonDecode[MonthDay] {
      def encode(x: MonthDay) = JString(x.toString)

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(MonthDay.parse(s))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }

    implicit object durationCodec extends JsonEncode[Duration] with JsonDecode[Duration] {
      // Due to a bug in Java 8 (https://bugs.openjdk.java.net/browse/JDK-8054978) this
      // actually doesn't work quite properly!
      val jdkDurationBug = Duration.parse("PT-0.5S").toString == "PT0.5S"

      private val problematicPattern = Pattern.compile("""^([^S]*[A-RT-Z])-0+\.([0-9]{1,9})*S$""", Pattern.CASE_INSENSITIVE)

      def encode(x: Duration) = JString(x.toString)

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            if(jdkDurationBug) safePath(s, jstr)
            else nativePath(s, jstr)
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }

      private def safePath(s: String, jstr: JString): Either[DecodeError, Duration] = {
        try {
          val m = problematicPattern.matcher(s)
          if(m.matches) {
            val goodPrefix = m.group(1)
            val badSuffixNanoStr = m.group(2)
            var scale = 1L
            var expt = 9-badSuffixNanoStr.length
            while(expt > 0) { scale *= 10; expt -= 1 }
            val badSuffixNanos = badSuffixNanoStr.toLong * scale
            if(goodPrefix.equalsIgnoreCase("PT")) {
              Right(Duration.ZERO.minusNanos(badSuffixNanos))
            } else {
              Right(Duration.parse(goodPrefix).minusNanos(badSuffixNanos))
            }
          } else {
            Right(Duration.parse(s))
          }
        } catch {
          case _ : ArithmeticException | _ : DateTimeParseException =>
            Left(DecodeError.InvalidValue(jstr))
        }
      }

      private def nativePath(s: String, jstr: JString): Either[DecodeError, Duration] = {
        try {
          Right(Duration.parse(s))
        } catch {
          case e: DateTimeParseException =>
            Left(DecodeError.InvalidValue(jstr))
        }
      }
    }

    implicit object periodCodec extends JsonEncode[Period] with JsonDecode[Period] {
      def encode(x: Period) = JString(x.toString)

      def decode(x: JValue) =
        x match {
          case jstr@JString(s) =>
            try {
              Right(Period.parse(s))
            } catch {
              case e: DateTimeParseException =>
                Left(DecodeError.InvalidValue(jstr))
            }
          case other =>
            Left(DecodeError.InvalidType(expected = JString, got = other.jsonType))
        }
    }
  }

  object encode {
    implicit val dateEncode : JsonEncode[Date] = codec.dateCodec
    implicit val instantEncode : JsonEncode[Instant] = codec.instantCodec
    implicit val offsetDateTimeEncode : JsonEncode[OffsetDateTime] = codec.offsetDateTimeCodec
    implicit val offsetTimeEncode : JsonEncode[OffsetTime] = codec.offsetTimeCodec
    implicit val zoneOffsetEncode : JsonEncode[ZoneOffset] = codec.zoneOffsetCodec
    implicit val localDateTimeEncode : JsonEncode[LocalDateTime] = codec.localDateTimeCodec
    implicit val localDateEncode : JsonEncode[LocalDate] = codec.localDateCodec
    implicit val localTimeEncode : JsonEncode[LocalTime] = codec.localTimeCodec
    implicit val yearMonthEncode : JsonEncode[YearMonth] = codec.yearMonthCodec
    implicit val monthDayEncode : JsonEncode[MonthDay] = codec.monthDayCodec
    implicit val durationEncode : JsonEncode[Duration] = codec.durationCodec
    implicit val periodEncode : JsonEncode[Period] = codec.periodCodec
  }

  object decode {
    implicit val dateDecode : JsonDecode[Date] = codec.dateCodec
    implicit val instantDecode : JsonDecode[Instant] = codec.instantCodec
    implicit val offsetDateTimeDecode : JsonDecode[OffsetDateTime] = codec.offsetDateTimeCodec
    implicit val offsetTimeDecode : JsonDecode[OffsetTime] = codec.offsetTimeCodec
    implicit val zoneOffsetDecode : JsonDecode[ZoneOffset] = codec.zoneOffsetCodec
    implicit val localDateTimeDecode : JsonDecode[LocalDateTime] = codec.localDateTimeCodec
    implicit val localDateDecode : JsonDecode[LocalDate] = codec.localDateCodec
    implicit val localTimeDecode : JsonDecode[LocalTime] = codec.localTimeCodec
    implicit val yearMonthDecode : JsonDecode[YearMonth] = codec.yearMonthCodec
    implicit val monthDayDecode : JsonDecode[MonthDay] = codec.monthDayCodec
    implicit val durationDecode : JsonDecode[Duration] = codec.durationCodec
    implicit val periodDecode : JsonDecode[Period] = codec.periodCodec
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy