diff --git a/build.sbt b/build.sbt index e068fa48..7bd9ecf8 100644 --- a/build.sbt +++ b/build.sbt @@ -87,7 +87,41 @@ lazy val sexpr = crossProject(JSPlatform, JVMPlatform, NativePlatform) libraryDependencies ++= Seq( "dev.zio" %%% "zio" % Version.zio, "dev.zio" %%% "zio-test" % Version.zio % Test - ) + ), + Compile / sourceGenerators += Def.task { + val dir = (Compile / sourceManaged).value + val file = dir / "zio" / "morphir" / "sexpr" / "GeneratedTupleDecoders.scala" + val decoders = (1 to 22).map { i => + val tparams = (1 to i).map(p => s"A$p").mkString(", ") + val implicits = (1 to i).map(p => s"A$p: SExprDecoder[A$p]").mkString(", ") + val work = (1 to i) + .map(p => s"val a$p = A$p.unsafeDecode(trace :+ traces($p), in)") + .mkString("\n Lexer.char(trace, in, ',')\n ") + val returns = (1 to i).map(p => s"a$p").mkString(", ") + + s"""implicit def tuple$i[$tparams](implicit $implicits): SExprDecoder[Tuple$i[$tparams]] = + | new SExprDecoder[Tuple$i[$tparams]] { + | val traces: Array[SExprError] = (0 to $i).map(SExprError.IndexedAccess(_)).toArray + | def unsafeDecode(trace: List[SExprError], in: RetractReader): Tuple$i[$tparams] = { + | Lexer.char(trace, in, '[') + | $work + | Lexer.char(trace, in, ']') + | Tuple$i($returns) + | } + | }""".stripMargin + } + IO.write( + file, + s"""package zio.morphir.sexpr + | + |import zio.morphir.sexpr.internal._ + | + |private[sexpr] trait GeneratedTupleDecoders { this: SExprDecoder.type => + | ${decoders.mkString("\n\n ")} + |}""".stripMargin + ) + Seq(file) + }.taskValue ) .enablePlugins(BuildInfoPlugin) diff --git a/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/decoder.scala b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/decoder.scala index c43c0e09..55c9a6f9 100644 --- a/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/decoder.scala +++ b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/decoder.scala @@ -2,75 +2,174 @@ package zio.morphir.sexpr import zio.morphir.sexpr.ast.SExpr import zio.morphir.sexpr.internal._ +import zio.morphir.sexpr.javatime.parsers +import zio.morphir.sexpr.uuid.UUIDParser +import zio.{Chunk, NonEmptyChunk} +import java.util.UUID +import scala.annotation._ +import scala.collection.immutable.{LinearSeq, ListSet, TreeSet} +import scala.collection.{immutable, mutable} import scala.util.control.NoStackTrace -trait SExprDecoder[A] { - self => +trait SExprDecoder[A] { self => + + /** + * An alias for [[SExprDecoder#orElse]]. + */ + final def <>[A1 >: A](that: => SExprDecoder[A1]): SExprDecoder[A1] = self.orElse(that) + + /** + * An alias for [[SExprDecoder#orElseEither]]. + */ + final def <+>[B](that: => SExprDecoder[B]): SExprDecoder[Either[A, B]] = self.orElseEither(that) + + /** + * An alias for [[SExprDecoder#zip]]. + */ + final def <*>[B](that: => SExprDecoder[B]): SExprDecoder[(A, B)] = self.zip(that) + + /** + * An alias for [[SExprDecoder#zipRight]]. + */ + final def *>[B](that: => SExprDecoder[B]): SExprDecoder[B] = self.zipRight(that) + + /** + * An alias for [[SExprDecoder#zipLeft]]. + */ + final def <*[B](that: => SExprDecoder[B]): SExprDecoder[A] = self.zipLeft(that) + + /** + * Attempts to decode a value of type `A` from the specified `CharSequence`, but may fail with + * a human-readable error message if the provided text does not encode a value of this type. + * + * Note: This method may not entirely consume the specified character sequence. + */ final def decodeSExpr(str: CharSequence): Either[String, A] = try Right(unsafeDecode(Nil, new FastStringReader(str))) catch { case SExprDecoder.UnsafeSExpr(trace) => Left(SExprError.render(trace)) - case _: UnexpectedEnd => - Left("Unexpected end of input") - case _: StackOverflowError => - Left("Unexpected structure") + case _: UnexpectedEnd => Left("Unexpected end of input") + case _: StackOverflowError => Left("Unexpected structure") } /** - * Returns a new decoder whose decoded values will be mapped by the specified function. + * Decode a value from an already parsed SExpr AST. + * + * The default implementation encodes the SExpr to a byte stream and uses decode to parse that. + * Override to provide a more performant implementation. */ - def map[B](f: A => B): SExprDecoder[B] = - new SExprDecoder[B] { + def fromAST(sexpr: SExpr): Either[String, A] = decodeSExpr(SExpr.encoder.encodeSExpr(sexpr, None)) - def unsafeDecode(trace: List[SExprError], in: RetractReader): B = - f(self.unsafeDecode(trace, in)) + /** + * Returns a new decoder whose decoded values will be mapped by the specified function. + */ + def map[B](f: A => B): SExprDecoder[B] = new SExprDecoder[B] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): B = + f(self.unsafeDecode(trace, in)) - override final def fromAST(sexpr: SExpr): Either[String, B] = - self.fromAST(sexpr).map(f) - } + override final def fromAST(sexpr: SExpr): Either[String, B] = + self.fromAST(sexpr).map(f) + } /** * Returns a new codec whose decoded values will be mapped by the specified function, which may * itself decide to fail with some type of error. */ final def mapOrFail[B](f: A => Either[String, B]): SExprDecoder[B] = new SExprDecoder[B] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) match { + case Left(err) => + throw SExprDecoder.UnsafeSExpr(SExprError.Message(err) :: trace) + case Right(b) => b + } - def unsafeDecode(trace: List[SExprError], in: RetractReader): B = - f(self.unsafeDecode(trace, in)) match { - case Left(err) => - throw SExprDecoder.UnsafeSExpr(SExprError.Message(err) :: trace) - case Right(b) => b + override final def fromAST(sexpr: SExpr): Either[String, B] = self.fromAST(sexpr).flatMap(f) + } + + /** + * Returns a new codec that combines this codec and the specified codec using fallback semantics: + * such that if this codec fails, the specified codec will be tried instead. + * This method may be unsafe from a security perspective: it can use more memory than hand coded + * alternative and so lead to DOS. + * + * For example, in the case of an alternative between `Int` and `Boolean`, a hand coded + * alternative would look like: + * + * ``` + * val decoder: SExprDecoder[AnyVal] = SExprDecoder.peekChar[AnyVal] { + * case 't' | 'f' => SExprDecoder[Boolean].widen + * case c => SExprDecoder[Int].widen + * } + * ``` + */ + final def orElse[A1 >: A](that: => SExprDecoder[A1]): SExprDecoder[A1] = new SExprDecoder[A1] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): A1 = { + val in2 = new zio.morphir.sexpr.internal.WithRecordingReader(in, 64) + + try self.unsafeDecode(trace, in2) + catch { + case SExprDecoder.UnsafeSExpr(_) => + in2.rewind() + that.unsafeDecode(trace, in2) + + case _: UnexpectedEnd => + in2.rewind() + that.unsafeDecode(trace, in2) } + } - override final def fromAST(sexpr: SExpr): Either[String, B] = - self.fromAST(sexpr).flatMap(f) + override final def fromAST(sexpr: SExpr): Either[String, A1] = + self.fromAST(sexpr) match { + case Left(_) => that.fromAST(sexpr) + case result @ Right(_) => result + } } + /** + * Returns a new codec that combines this codec and the specified codec using fallback semantics: + * such that if this codec fails, the specified codec will be tried instead. + */ + final def orElseEither[B](that: => SExprDecoder[B]): SExprDecoder[Either[A, B]] = + self.map(Left(_)).orElse(that.map(Right(_))) + /** * Low-level, unsafe method to decode a value or throw an exception. This method should not be * called in application code, although it can be implemented for user-defined data structures. */ def unsafeDecode(trace: List[SExprError], in: RetractReader): A + def unsafeDecodeMissing(trace: List[SExprError]): A = + throw SExprDecoder.UnsafeSExpr(SExprError.Message("missing") :: trace) + /** * Returns this decoder but widened to the its given super-type */ final def widen[B >: A]: SExprDecoder[B] = self.asInstanceOf[SExprDecoder[B]] /** - * Decode a value from an already parsed SExpr AST. - * - * The default implementation encodes the Json to a byte stream and uses decode to parse that. - * Override to provide a more performant implementation. + * Returns a new codec that combines this codec and the specified codec into a single codec that + * decodes a tuple of the values decoded by the respective codecs. + */ + final def zip[B](that: => SExprDecoder[B]): SExprDecoder[(A, B)] = SExprDecoder.tuple2(this, that) + + /** + * Zips two codecs, but discards the output on the right hand side. + */ + final def zipLeft[B](that: => SExprDecoder[B]): SExprDecoder[A] = self.zipWith(that)((a, _) => a) + + /** + * Zips two codecs, but discards the output on the left hand side. */ - def fromAST(sexpr: SExpr): Either[String, A] = - decodeSExpr(SExpr.encoder.encodeSExpr(sexpr, None)) + final def zipRight[B](that: => SExprDecoder[B]): SExprDecoder[B] = self.zipWith(that)((_, b) => b) + /** + * Zips two codecs into one, transforming the outputs of both codecs by the specified function. + */ + final def zipWith[B, C](that: => SExprDecoder[B])(f: (A, B) => C): SExprDecoder[C] = self.zip(that).map(f.tupled) } -object SExprDecoder { +object SExprDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 { type SExprError = zio.morphir.sexpr.SExprError val SExprError = zio.morphir.sexpr.SExprError @@ -93,31 +192,342 @@ object SExprDecoder { } implicit val string: SExprDecoder[String] = new SExprDecoder[String] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): String = Lexer.string(trace, in).toString + + override final def fromAST(sexpr: SExpr): Either[String, String] = sexpr match { + case SExpr.Str(value) => Right(value) + case _ => Left("Not a string value") + } + } - def unsafeDecode(trace: List[SExprError], in: RetractReader): String = - Lexer.string(trace, in).toString + implicit val boolean: SExprDecoder[Boolean] = new SExprDecoder[Boolean] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): Boolean = Lexer.boolean(trace, in) - override final def fromAST(sexpr: SExpr): Either[String, String] = - sexpr match { - case SExpr.Str(value) => Right(value) - case _ => Left("Not a string value") + override final def fromAST(sexpr: SExpr): Either[String, Boolean] = sexpr match { + case SExpr.Bool(value) => Right(value) + case _ => Left("Not a bool value") + } + } + + //TODO: We may want to support Clojure style Character literals instead + implicit val char: SExprDecoder[Char] = string.mapOrFail { + case str if str.length == 1 => Right(str(0)) + case _ => Left("expected one character") + } + implicit val symbol: SExprDecoder[Symbol] = string.map(Symbol(_)) + implicit val byte: SExprDecoder[Byte] = number(Lexer.byte, _.byteValueExact()) + implicit val short: SExprDecoder[Short] = number(Lexer.short, _.shortValueExact()) + implicit val int: SExprDecoder[Int] = number(Lexer.int, _.intValueExact()) + implicit val long: SExprDecoder[Long] = number(Lexer.long, _.longValueExact()) + implicit val bigInteger: SExprDecoder[java.math.BigInteger] = number(Lexer.bigInteger, _.toBigIntegerExact) + implicit val scalaBigInt: SExprDecoder[BigInt] = bigInteger.map(x => x) + implicit val float: SExprDecoder[Float] = number(Lexer.float, _.floatValue()) + implicit val double: SExprDecoder[Double] = number(Lexer.double, _.doubleValue()) + implicit val bigDecimal: SExprDecoder[java.math.BigDecimal] = number(Lexer.bigDecimal, identity) + implicit val scalaBigDecimal: SExprDecoder[BigDecimal] = bigDecimal.map(x => x) + + // numbers decode from numbers or strings for maximum compatibility + private[this] def number[A]( + f: (List[SExprError], RetractReader) => A, + fromBigDecimal: java.math.BigDecimal => A + ): SExprDecoder[A] = + new SExprDecoder[A] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): A = (in.nextNonWhitespace(): @switch) match { + case '"' => + val i = f(trace, in) + Lexer.charOnly(trace, in, '"') + i + case _ => + in.retract() + f(trace, in) + } + + override final def fromAST(sexpr: SExpr): Either[String, A] = sexpr match { + case SExpr.Num(value) => + try Right(fromBigDecimal(value)) + catch { + case exception: ArithmeticException => Left(exception.getMessage) + } + case SExpr.Str(value) => + val reader = new FastStringReader(value) + val result = + try Right(f(List.empty, reader)) + catch { + case SExprDecoder.UnsafeSExpr(trace) => Left(SExprError.render(trace)) + case _: UnexpectedEnd => Left("Unexpected end of input") + case _: StackOverflowError => Left("Unexpected structure") + } finally reader.close() + result + case _ => Left("Not a number or a string") } + } + // Option treats empty and null values as Nothing and passes values to the decoder. + // + // If alternative behaviour is desired, e.g. pass null to the underlying, then + // use a newtype wrapper. + implicit def option[A](implicit A: SExprDecoder[A]): SExprDecoder[Option[A]] = new SExprDecoder[Option[A]] { self => + private[this] val ull: Array[Char] = "ull".toCharArray + + override def unsafeDecodeMissing(trace: List[SExprError]): Option[A] = Option.empty + + def unsafeDecode(trace: List[SExprError], in: RetractReader): Option[A] = + (in.nextNonWhitespace(): @switch) match { + case 'n' => + Lexer.readChars(trace, in, ull, "null") + None + case _ => + in.retract() + Some(A.unsafeDecode(trace, in)) + } + + // overridden here to pass `None` to the new Decoder instead of throwing + // when called from a derived decoder + override def map[B](f: Option[A] => B): SExprDecoder[B] = new SExprDecoder[B] { + override def unsafeDecodeMissing(trace: List[SExprError]): B = + f(None) + + def unsafeDecode(trace: List[SExprError], in: RetractReader): B = + f(self.unsafeDecode(trace, in)) + + override final def fromAST(sexpr: SExpr): Either[String, B] = + self.fromAST(sexpr).map(f) + } + + override final def fromAST(sexpr: SExpr): Either[String, Option[A]] = sexpr match { + //case SExpr.Null => Right(None) // TODO need to decide what to do for null + case _ => A.fromAST(sexpr).map(Some.apply) + } } + /* TO DO need to figure out how to do Either + // supports multiple representations for compatibility with other libraries, + // but does not support the "discriminator field" encoding with a field named + // "value" used by some libraries. + implicit def either[A, B](implicit + A: SExprDecoder[A], + B: SExprDecoder[B] + ): SExprDecoder[Either[A, B]] = + new SExprDecoder[Either[A, B]] { - implicit val boolean: SExprDecoder[Boolean] = new SExprDecoder[Boolean] { + val names: Array[String] = + Array("a", "Left", "left", "b", "Right", "right") + val matrix: StringMatrix = new StringMatrix(names) + val spans: Array[SExprError] = names.map(SExprError.ObjectAccess) - def unsafeDecode(trace: List[SExprError], in: RetractReader): Boolean = - Lexer.boolean(trace, in) + def unsafeDecode( + trace: List[SExprError], + in: RetractReader + ): Either[A, B] = { + Lexer.char(trace, in, '{') - override final def fromAST(sexpr: SExpr): Either[String, Boolean] = + val values: Array[Any] = Array.ofDim(2) + + if (Lexer.firstField(trace, in)) + while ({ + { + val field = Lexer.field(trace, in, matrix) + if (field == -1) Lexer.skipValue(trace, in) + else { + val trace_ = spans(field) :: trace + if (field < 3) { + if (values(0) != null) + throw UnsafeSExpr(SExprError.Message("duplicate") :: trace_) + values(0) = A.unsafeDecode(trace_, in) + } else { + if (values(1) != null) + throw UnsafeSExpr(SExprError.Message("duplicate") :: trace_) + values(1) = B.unsafeDecode(trace_, in) + } + } + }; Lexer.nextField(trace, in) + }) () + + if (values(0) == null && values(1) == null) + throw UnsafeSExpr(SExprError.Message("missing fields") :: trace) + if (values(0) != null && values(1) != null) + throw UnsafeSExpr( + SExprError.Message("ambiguous either, both present") :: trace + ) + if (values(0) != null) + Left(values(0).asInstanceOf[A]) + else Right(values(1).asInstanceOf[B]) + } + } + */ + private[sexpr] def builder[A, T[_]](trace: List[SExprError], in: RetractReader, builder: mutable.Builder[A, T[A]])( + implicit A: SExprDecoder[A] + ): T[A] = { + Lexer.char(trace, in, '[') + var i: Int = 0 + if (Lexer.firstArrayElement(in)) while ({ + { + val trace_ = SExprError.IndexedAccess(i) :: trace + builder += A.unsafeDecode(trace_, in) + i += 1 + }; Lexer.nextArrayElement(trace, in) + }) () + builder.result() + } + + // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) + private[sexpr] def mapStringOrFail[A](f: String => Either[String, A]): SExprDecoder[A] = new SExprDecoder[A] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): A = + f(string.unsafeDecode(trace, in)) match { + case Left(err) => throw UnsafeSExpr(SExprError.Message(err) :: trace) + case Right(value) => value + } + + override def fromAST(sexpr: SExpr): Either[String, A] = + string.fromAST(sexpr).flatMap(f) + } +} + +private[sexpr] trait DecoderLowPriority1 extends DecoderLowPriority2 { this: SExprDecoder.type => + implicit def array[A: SExprDecoder: reflect.ClassTag]: SExprDecoder[Array[A]] = new SExprDecoder[Array[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): Array[A] = + builder(trace, in, Array.newBuilder[A]) + } + + implicit def seq[A: SExprDecoder]: SExprDecoder[Seq[A]] = new SExprDecoder[Seq[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): Seq[A] = + builder(trace, in, immutable.Seq.newBuilder[A]) + } + + implicit def chunk[A: SExprDecoder]: SExprDecoder[Chunk[A]] = new SExprDecoder[Chunk[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): Chunk[A] = + builder(trace, in, zio.ChunkBuilder.make[A]()) + + override final def fromAST(sexpr: SExpr): Either[String, Chunk[A]] = sexpr match { - case SExpr.Bool(value) => Right(value) - case _ => Left("Not a bool value") + case SExpr.SVector(elements) => + elements.foldLeft[Either[String, Chunk[A]]](Right(Chunk.empty)) { (s, item) => + s.flatMap(chunk => + implicitly[SExprDecoder[A]].fromAST(item).map { a => + chunk :+ a + } + ) + } + case _ => Left("Not an array") } } - implicit val char: SExprDecoder[Char] = string.mapOrFail { - case str if str.length == 1 => Right(str(0)) - case _ => Left("expected one character") + implicit def nonEmptyChunk[A: SExprDecoder]: SExprDecoder[NonEmptyChunk[A]] = + chunk[A].mapOrFail(NonEmptyChunk.fromChunk(_).toRight("Chunk was empty")) + + implicit def indexedSeq[A: SExprDecoder]: SExprDecoder[IndexedSeq[A]] = new SExprDecoder[IndexedSeq[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): IndexedSeq[A] = + builder(trace, in, IndexedSeq.newBuilder[A]) + } + + implicit def linearSeq[A: SExprDecoder]: SExprDecoder[immutable.LinearSeq[A]] = + new SExprDecoder[immutable.LinearSeq[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): LinearSeq[A] = + builder(trace, in, immutable.LinearSeq.newBuilder[A]) + } + + implicit def listSet[A: SExprDecoder]: SExprDecoder[immutable.ListSet[A]] = new SExprDecoder[immutable.ListSet[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): ListSet[A] = + builder(trace, in, immutable.ListSet.newBuilder[A]) + } + + implicit def treeSet[A: SExprDecoder: Ordering]: SExprDecoder[immutable.TreeSet[A]] = + new SExprDecoder[immutable.TreeSet[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): TreeSet[A] = + builder(trace, in, immutable.TreeSet.newBuilder[A]) + } + + implicit def list[A: SExprDecoder]: SExprDecoder[List[A]] = new SExprDecoder[List[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): List[A] = + builder(trace, in, new mutable.ListBuffer[A]) + } + + implicit def vector[A: SExprDecoder]: SExprDecoder[Vector[A]] = new SExprDecoder[Vector[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): Vector[A] = + builder(trace, in, immutable.Vector.newBuilder[A]) + } + + implicit def set[A: SExprDecoder]: SExprDecoder[Set[A]] = new SExprDecoder[Set[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): Set[A] = + builder(trace, in, Set.newBuilder[A]) + } + + implicit def hashSet[A: SExprDecoder]: SExprDecoder[immutable.HashSet[A]] = new SExprDecoder[immutable.HashSet[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): immutable.HashSet[A] = + builder(trace, in, immutable.HashSet.newBuilder[A]) + } + + implicit def sortedSet[A: Ordering: SExprDecoder]: SExprDecoder[immutable.SortedSet[A]] = + new SExprDecoder[immutable.SortedSet[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): immutable.SortedSet[A] = + builder(trace, in, immutable.SortedSet.newBuilder[A]) + } + +} +// We have a hierarchy of implicits for two reasons: +// +// 1. the compiler searches each scope and returns early if it finds a match. +// This means that it is faster to put more complex derivation rules (that +// are unlikely to be commonly used) into a lower priority scope, allowing +// simple things like primitives to match fast. +// +// 2. sometimes we want to have overlapping instances with a more specific / +// optimised instances, and a fallback for the more general case that would +// otherwise conflict in a lower priority scope. A good example of this is to +// have specialised decoders for collection types, falling back to BuildFrom. +private[sexpr] trait DecoderLowPriority2 extends DecoderLowPriority3 { this: SExprDecoder.type => + implicit def iterable[A: SExprDecoder]: SExprDecoder[Iterable[A]] = new SExprDecoder[Iterable[A]] { + def unsafeDecode(trace: List[SExprError], in: RetractReader): Iterable[A] = + builder(trace, in, immutable.Iterable.newBuilder[A]) } } + +private[sexpr] trait DecoderLowPriority3 { this: SExprDecoder.type => + import java.time.format.DateTimeParseException + import java.time.zone.ZoneRulesException + import java.time._ + + implicit val dayOfWeek: SExprDecoder[DayOfWeek] = + mapStringOrFail(s => parseJavaTime(DayOfWeek.valueOf, s.toUpperCase)) + implicit val duration: SExprDecoder[Duration] = mapStringOrFail(parseJavaTime(parsers.unsafeParseDuration, _)) + implicit val instant: SExprDecoder[Instant] = mapStringOrFail(parseJavaTime(parsers.unsafeParseInstant, _)) + implicit val localDate: SExprDecoder[LocalDate] = mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalDate, _)) + + implicit val localDateTime: SExprDecoder[LocalDateTime] = + mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalDateTime, _)) + + implicit val localTime: SExprDecoder[LocalTime] = mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalTime, _)) + implicit val month: SExprDecoder[Month] = mapStringOrFail(s => parseJavaTime(Month.valueOf, s.toUpperCase)) + implicit val monthDay: SExprDecoder[MonthDay] = mapStringOrFail(parseJavaTime(parsers.unsafeParseMonthDay, _)) + + implicit val offsetDateTime: SExprDecoder[OffsetDateTime] = + mapStringOrFail(parseJavaTime(parsers.unsafeParseOffsetDateTime, _)) + + implicit val offsetTime: SExprDecoder[OffsetTime] = mapStringOrFail(parseJavaTime(parsers.unsafeParseOffsetTime, _)) + implicit val period: SExprDecoder[Period] = mapStringOrFail(parseJavaTime(parsers.unsafeParsePeriod, _)) + implicit val year: SExprDecoder[Year] = mapStringOrFail(parseJavaTime(parsers.unsafeParseYear, _)) + implicit val yearMonth: SExprDecoder[YearMonth] = mapStringOrFail(parseJavaTime(parsers.unsafeParseYearMonth, _)) + + implicit val zonedDateTime: SExprDecoder[ZonedDateTime] = mapStringOrFail( + parseJavaTime(parsers.unsafeParseZonedDateTime, _) + ) + + implicit val zoneId: SExprDecoder[ZoneId] = mapStringOrFail(parseJavaTime(parsers.unsafeParseZoneId, _)) + implicit val zoneOffset: SExprDecoder[ZoneOffset] = mapStringOrFail(parseJavaTime(parsers.unsafeParseZoneOffset, _)) + + // Commonized handling for decoding from string to java.time Class + private[sexpr] def parseJavaTime[A](f: String => A, s: String): Either[String, A] = + try Right(f(s)) + catch { + case zre: ZoneRulesException => Left(s"$s is not a valid ISO-8601 format, ${zre.getMessage}") + case dtpe: DateTimeParseException => Left(s"$s is not a valid ISO-8601 format, ${dtpe.getMessage}") + case dte: DateTimeException => Left(s"$s is not a valid ISO-8601 format, ${dte.getMessage}") + case ex: Exception => Left(ex.getMessage) + } + + implicit val uuid: SExprDecoder[UUID] = + mapStringOrFail { str => + try Right(UUIDParser.unsafeParse(str)) + catch { + case iae: IllegalArgumentException => Left(s"Invalid UUID: ${iae.getMessage}") + } + } +} diff --git a/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/internal/lexer.scala b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/internal/lexer.scala index e154972f..10121a6d 100644 --- a/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/internal/lexer.scala +++ b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/internal/lexer.scala @@ -96,6 +96,109 @@ object Lexer { ) } + def byte(trace: List[SExprError], in: RetractReader): Byte = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.byte_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message("expected a Byte") :: trace) + } + } + + def short(trace: List[SExprError], in: RetractReader): Short = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.short_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message("expected a Short") :: trace) + } + } + + def int(trace: List[SExprError], in: RetractReader): Int = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.int_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message("expected an Int") :: trace) + } + } + + def long(trace: List[SExprError], in: RetractReader): Long = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.long_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message("expected a Long") :: trace) + } + } + + def bigInteger( + trace: List[SExprError], + in: RetractReader + ): java.math.BigInteger = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message(s"expected a $NumberMaxBits bit BigInteger") :: trace) + } + } + + def float(trace: List[SExprError], in: RetractReader): Float = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.float_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message("expected a Float") :: trace) + } + } + + def double(trace: List[SExprError], in: RetractReader): Double = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.double_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message("expected a Double") :: trace) + } + } + + def bigDecimal( + trace: List[SExprError], + in: RetractReader + ): java.math.BigDecimal = { + checkNumber(trace, in) + try { + val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => + throw UnsafeSExpr(SExprError.Message(s"expected a $NumberMaxBits BigDecimal") :: trace) + } + } + + // optional whitespace and then an expected character @inline def char(trace: List[SExprError], in: OneCharReader, c: Char): Unit = { val got = in.nextNonWhitespace() @@ -121,6 +224,18 @@ object Lexer { case _ => false } + // really just a way to consume the whitespace + private def checkNumber(trace: List[SExprError], in: RetractReader): Unit = { + (in.nextNonWhitespace(): @switch) match { + case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => () + case c => + throw UnsafeSExpr( + SExprError.Message(s"expected a number, got $c") :: trace + ) + } + in.retract() + } + def readChars( trace: List[SExprError], in: OneCharReader, diff --git a/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/javatime/parsers.scala b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/javatime/parsers.scala new file mode 100644 index 00000000..7e204b6d --- /dev/null +++ b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/javatime/parsers.scala @@ -0,0 +1,1518 @@ +package zio.morphir.sexpr.javatime + +import java.time.{ + DateTimeException, + Duration, + Instant, + LocalDate, + LocalDateTime, + LocalTime, + MonthDay, + OffsetDateTime, + OffsetTime, + Period, + Year, + YearMonth, + ZoneId, + ZoneOffset, + ZonedDateTime +} +import java.util.concurrent.ConcurrentHashMap +import scala.annotation.switch +import scala.util.control.NoStackTrace + +private[sexpr] object parsers { + private[this] final val zoneOffsets: Array[ZoneOffset] = new Array(145) + private[this] final val zoneIds: ConcurrentHashMap[String, ZoneId] = new ConcurrentHashMap(256) + + def unsafeParseDuration(input: String): Duration = { + val len = input.length + var pos = 0 + var seconds = 0L + var nanos, state = 0 + if (pos >= len) durationError(pos) + var ch = input.charAt(pos) + pos += 1 + val isNeg = ch == '-' + if (isNeg) { + if (pos >= len) durationError(pos) + ch = input.charAt(pos) + pos += 1 + } + if (ch != 'P') durationOrPeriodStartError(isNeg, pos - 1) + if (pos >= len) durationError(pos) + ch = input.charAt(pos) + pos += 1 + while ({ + if (state == 0) { + if (ch == 'T') { + if (pos >= len) durationError(pos) + ch = input.charAt(pos) + pos += 1 + state = 1 + } + } else if (state == 1) { + if (ch != 'T') charsError('T', '"', pos - 1) + if (pos >= len) durationError(pos) + ch = input.charAt(pos) + pos += 1 + } else if (state == 4 && pos >= len) durationError(pos - 1) + val isNegX = ch == '-' + if (isNegX) { + if (pos >= len) durationError(pos) + ch = input.charAt(pos) + pos += 1 + } + if (ch < '0' || ch > '9') durationOrPeriodDigitError(isNegX, state <= 1, pos - 1) + var x: Long = ('0' - ch).toLong + while ( + (pos < len) && { + ch = input.charAt(pos) + ch >= '0' && ch <= '9' + } + ) { + if ( + x < -922337203685477580L || { + x = x * 10 + ('0' - ch) + x > 0 + } + ) durationError(pos) + pos += 1 + } + if (!(isNeg ^ isNegX)) { + if (x == -9223372036854775808L) durationError(pos) + x = -x + } + if (ch == 'D' && state <= 0) { + if (x < -106751991167300L || x > 106751991167300L) + durationError(pos) // -106751991167300L == Long.MinValue / 86400 + seconds = x * 86400 + state = 1 + } else if (ch == 'H' && state <= 1) { + if (x < -2562047788015215L || x > 2562047788015215L) + durationError(pos) // -2562047788015215L == Long.MinValue / 3600 + seconds = sumSeconds(x * 3600, seconds, pos) + state = 2 + } else if (ch == 'M' && state <= 2) { + if (x < -153722867280912930L || x > 153722867280912930L) + durationError(pos) // -153722867280912930L == Long.MinValue / 60 + seconds = sumSeconds(x * 60, seconds, pos) + state = 3 + } else if (ch == '.') { + pos += 1 + seconds = sumSeconds(x, seconds, pos) + var nanoDigitWeight = 100000000 + while ( + (pos < len) && { + ch = input.charAt(pos) + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nanos += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + pos += 1 + } + if (ch != 'S') nanoError(nanoDigitWeight, 'S', pos) + if (isNeg ^ isNegX) nanos = -nanos + state = 4 + } else if (ch == 'S') { + seconds = sumSeconds(x, seconds, pos) + state = 4 + } else durationError(state, pos) + pos += 1 + (pos < len) && { + ch = input.charAt(pos) + pos += 1 + true + } + }) () + Duration.ofSeconds(seconds, nanos.toLong) + } + + def unsafeParseInstant(input: String): Instant = { + val len = input.length + var pos = 0 + val year = { + if (pos + 4 >= len) instantError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + if (ch0 >= '0' && ch0 <= '9') { + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 != '-') charError('-', pos + 4) + pos += 5 + ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + pos += 5 + var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + var yearDigits = 4 + var ch: Char = '0' + while ({ + if (pos >= len) instantError(pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 10 + }) { + year = + if (year > 100000000) 2147483647 + else year * 10 + (ch - '0') + yearDigits += 1 + } + if (yearDigits == 10 && year > 1000000000) yearError(pos - 2) + if (yearNeg) { + if (year == 0) yearError(pos - 2) + year = -year + } + if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) + year + } + } + val month = { + if (pos + 2 >= len) instantError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (month < 1 || month > 12) monthError(pos + 1) + if (ch2 != '-') charError('-', pos + 2) + pos += 3 + month + } + val day = { + if (pos + 2 >= len) instantError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) + if (ch2 != 'T') charError('T', pos + 2) + pos += 3 + day + } + val hour = { + if (pos + 2 >= len) instantError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (hour > 23) hourError(pos + 1) + if (ch2 != ':') charError(':', pos + 2) + pos += 3 + hour + } + val minute = { + if (pos + 1 >= len) instantError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') minuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + var nanoDigitWeight = -1 + var second, nano = 0 + var ch = (0: Char) + if (pos < len) { + ch = input.charAt(pos) + pos += 1 + if (ch == ':') { + nanoDigitWeight = -2 + second = { + if (pos + 1 >= len) instantError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') secondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if (pos < len) { + ch = input.charAt(pos) + pos += 1 + if (ch == '.') { + nanoDigitWeight = 100000000 + while ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + } + } + if (ch != 'Z') instantError(nanoDigitWeight, pos - 1) + if (pos != len) instantError(pos) + val epochDay = + epochDayForYear(year) + (dayOfYearForYearMonth(year, month) + day - 719529) // 719528 == days 0000 to 1970 + Instant.ofEpochSecond( + epochDay * 86400 + (hour * 3600 + minute * 60 + second), + nano.toLong + ) // 86400 == seconds per day + } + + def unsafeParseLocalDate(input: String): LocalDate = { + val len = input.length + var pos = 0 + val year = { + if (pos + 4 >= len) localDateError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + if (ch0 >= '0' && ch0 <= '9') { + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 != '-') charError('-', pos + 4) + pos += 5 + ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + pos += 5 + var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + var yearDigits = 4 + var ch: Char = '0' + while ({ + if (pos >= len) localDateError(pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + if (yearNeg) { + if (year == 0) yearError(pos - 2) + year = -year + } + if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) + year + } + } + val month = { + if (pos + 2 >= len) localDateError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (month < 1 || month > 12) monthError(pos + 1) + if (ch2 != '-') charError('-', pos + 2) + pos += 3 + month + } + val day = { + if (pos + 1 >= len) localDateError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) + pos += 2 + day + } + if (pos != len) localDateError(pos) + LocalDate.of(year, month, day) + } + + def unsafeParseLocalDateTime(input: String): LocalDateTime = { + val len = input.length + var pos = 0 + val year = { + if (pos + 4 >= len) localDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + if (ch0 >= '0' && ch0 <= '9') { + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 != '-') charError('-', pos + 4) + pos += 5 + ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + pos += 5 + var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + var yearDigits = 4 + var ch: Char = '0' + while ({ + if (pos >= len) localDateTimeError(pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + if (yearNeg) { + if (year == 0) yearError(pos - 2) + year = -year + } + if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) + year + } + } + val month = { + if (pos + 2 >= len) localDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (month < 1 || month > 12) monthError(pos + 1) + if (ch2 != '-') charError('-', pos + 2) + pos += 3 + month + } + val day = { + if (pos + 2 >= len) localDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) + if (ch2 != 'T') charError('T', pos + 2) + pos += 3 + day + } + val hour = { + if (pos + 2 >= len) localDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (hour > 23) hourError(pos + 1) + if (ch2 != ':') charError(':', pos + 2) + pos += 3 + hour + } + val minute = { + if (pos + 1 >= len) localDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') minuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + var second, nano = 0 + if (pos < len) { + if (input.charAt(pos) != ':') charError(':', pos) + pos += 1 + second = { + if (pos + 1 >= len) localDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') secondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if (pos < len) { + if (input.charAt(pos) != '.') charError('.', pos) + pos += 1 + var nanoDigitWeight = 100000000 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + if (pos != len || ch < '0' || ch > '9') localDateTimeError(pos - 1) + } + } + LocalDateTime.of(year, month, day, hour, minute, second, nano) + } + + def unsafeParseLocalTime(input: String): LocalTime = { + val len = input.length + var pos = 0 + val hour = { + if (pos + 2 >= len) localTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (hour > 23) hourError(pos + 1) + if (ch2 != ':') charError(':', pos + 2) + pos += 3 + hour + } + val minute = { + if (pos + 1 >= len) localTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') minuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + var second, nano = 0 + if (pos < len) { + if (input.charAt(pos) != ':') charError(':', pos) + pos += 1 + second = { + if (pos + 1 >= len) localTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') secondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if (pos < len) { + if (input.charAt(pos) != '.') charError('.', pos) + pos += 1 + var nanoDigitWeight = 100000000 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + if (pos != len || ch < '0' || ch > '9') localTimeError(pos - 1) + } + } + LocalTime.of(hour, minute, second, nano) + } + + def unsafeParseMonthDay(input: String): MonthDay = { + if (input.length != 7) error("illegal month day", 0) + val ch0 = input.charAt(0) + val ch1 = input.charAt(1) + val ch2 = input.charAt(2) + val ch3 = input.charAt(3) + val ch4 = input.charAt(4) + val ch5 = input.charAt(5) + val ch6 = input.charAt(6) + val month = ch2 * 10 + ch3 - 528 // 528 == '0' * 11 + val day = ch5 * 10 + ch6 - 528 // 528 == '0' * 11 + if (ch0 != '-') charError('-', 0) + if (ch1 != '-') charError('-', 1) + if (ch2 < '0' || ch2 > '9') digitError(2) + if (ch3 < '0' || ch3 > '9') digitError(3) + if (month < 1 || month > 12) monthError(3) + if (ch4 != '-') charError('-', 4) + if (ch5 < '0' || ch5 > '9') digitError(5) + if (ch6 < '0' || ch6 > '9') digitError(6) + if (day == 0 || (day > 28 && day > maxDayForMonth(month))) dayError(6) + MonthDay.of(month, day) + } + + def unsafeParseOffsetDateTime(input: String): OffsetDateTime = { + val len = input.length + var pos = 0 + val year = { + if (pos + 4 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + if (ch0 >= '0' && ch0 <= '9') { + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 != '-') charError('-', pos + 4) + pos += 5 + ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + pos += 5 + var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + var yearDigits = 4 + var ch: Char = '0' + while ({ + if (pos >= len) offsetDateTimeError(pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + if (yearNeg) { + if (year == 0) yearError(pos - 2) + year = -year + } + if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) + year + } + } + val month = { + if (pos + 2 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (month < 1 || month > 12) monthError(pos + 1) + if (ch2 != '-') charError('-', pos + 2) + pos += 3 + month + } + val day = { + if (pos + 2 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) + if (ch2 != 'T') charError('T', pos + 2) + pos += 3 + day + } + val hour = { + if (pos + 2 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (hour > 23) hourError(pos + 1) + if (ch2 != ':') charError(':', pos + 2) + pos += 3 + hour + } + val minute = { + if (pos + 1 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') minuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + var second, nano = 0 + var nanoDigitWeight = -1 + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + var ch = input.charAt(pos) + pos += 1 + if (ch == ':') { + nanoDigitWeight = -2 + second = { + if (pos + 1 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') secondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + ch = input.charAt(pos) + pos += 1 + if (ch == '.') { + nanoDigitWeight = 100000000 + while ({ + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + }) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + val zoneOffset = + if (ch == 'Z') ZoneOffset.UTC + else { + val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) + val offsetHour = { + if (pos + 1 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (offsetHour > 18) timezoneOffsetHourError(pos + 1) + pos += 2 + offsetHour + } + var offsetMinute, offsetSecond = 0 + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' + } + ) { + offsetMinute = { + if (pos + 1 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' + } + ) { + offsetSecond = { + if (pos + 1 >= len) offsetDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetSecondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + } + } + toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + } + if (pos != len) offsetDateTimeError(pos) + OffsetDateTime.of(year, month, day, hour, minute, second, nano, zoneOffset) + } + + def unsafeParseOffsetTime(input: String): OffsetTime = { + val len = input.length + var pos = 0 + val hour = { + if (pos + 2 >= len) offsetTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (hour > 23) hourError(pos + 1) + if (ch2 != ':') charError(':', pos + 2) + pos += 3 + hour + } + val minute = { + if (pos + 1 >= len) offsetTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') minuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + var second, nano = 0 + var nanoDigitWeight = -1 + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + var ch = input.charAt(pos) + pos += 1 + if (ch == ':') { + nanoDigitWeight = -2 + second = { + if (pos + 1 >= len) offsetTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') secondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + ch = input.charAt(pos) + pos += 1 + if (ch == '.') { + nanoDigitWeight = 100000000 + while ({ + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + }) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + val zoneOffset = + if (ch == 'Z') ZoneOffset.UTC + else { + val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) + nanoDigitWeight = -3 + val offsetHour = { + if (pos + 1 >= len) offsetTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (offsetHour > 18) timezoneOffsetHourError(pos + 1) + pos += 2 + offsetHour + } + var offsetMinute, offsetSecond = 0 + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' + } + ) { + offsetMinute = { + if (pos + 1 >= len) offsetTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' + } + ) { + nanoDigitWeight = -4 + offsetSecond = { + if (pos + 1 >= len) offsetTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetSecondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + } + } + toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + } + if (pos != len) offsetTimeError(pos) + OffsetTime.of(hour, minute, second, nano, zoneOffset) + } + + def unsafeParsePeriod(input: String): Period = { + val len = input.length + var pos, state, years, months, days = 0 + if (pos >= len) periodError(pos) + var ch = input.charAt(pos) + pos += 1 + val isNeg = ch == '-' + if (isNeg) { + if (pos >= len) periodError(pos) + ch = input.charAt(pos) + pos += 1 + } + if (ch != 'P') durationOrPeriodStartError(isNeg, pos - 1) + if (pos >= len) periodError(pos) + ch = input.charAt(pos) + pos += 1 + while ({ + if (state == 4 && pos >= len) periodError(pos - 1) + val isNegX = ch == '-' + if (isNegX) { + if (pos >= len) periodError(pos) + ch = input.charAt(pos) + pos += 1 + } + if (ch < '0' || ch > '9') durationOrPeriodDigitError(isNegX, state <= 1, pos - 1) + var x: Int = '0' - ch + while ( + (pos < len) && { + ch = input.charAt(pos) + ch >= '0' && ch <= '9' + } + ) { + if ( + x < -214748364 || { + x = x * 10 + ('0' - ch) + x > 0 + } + ) periodError(pos) + pos += 1 + } + if (!(isNeg ^ isNegX)) { + if (x == -2147483648) periodError(pos) + x = -x + } + if (ch == 'Y' && state <= 0) { + years = x + state = 1 + } else if (ch == 'M' && state <= 1) { + months = x + state = 2 + } else if (ch == 'W' && state <= 2) { + if (x < -306783378 || x > 306783378) periodError(pos) + days = x * 7 + state = 3 + } else if (ch == 'D') { + val ds = x.toLong + days + if (ds != ds.toInt) periodError(pos) + days = ds.toInt + state = 4 + } else periodError(state, pos) + pos += 1 + (pos < len) && { + ch = input.charAt(pos) + pos += 1 + true + } + }) () + Period.of(years, months, days) + } + + def unsafeParseYear(input: String): Year = { + val len = input.length + var pos = 0 + val year = { + if (pos + 3 >= len) yearError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + if (ch0 >= '0' && ch0 <= '9') { + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (len != 4) yearError(pos + 4) + pos += 4 + ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + pos += 4 + var year = ch1 * 100 + ch2 * 10 + ch3 - 5328 // 53328 == '0' * 111 + var yearDigits = 3 + var ch: Char = '0' + while ( + pos < len && { + ch = input.charAt(pos) + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + pos += 1 + } + if (yearNeg) { + if (year == 0) yearError(pos - 1) + year = -year + } + if (pos != len || ch < '0' || ch > '9') { + if (yearDigits == 9) yearError(pos) + digitError(pos) + } + year + } + } + Year.of(year) + } + + def unsafeParseYearMonth(input: String): YearMonth = { + val len = input.length + var pos = 0 + val year = { + if (pos + 4 >= len) yearMonthError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + if (ch0 >= '0' && ch0 <= '9') { + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 != '-') charError('-', pos + 4) + pos += 5 + ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + pos += 5 + var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + var yearDigits = 4 + var ch: Char = '0' + while ({ + if (pos >= len) yearMonthError(pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + if (yearNeg) { + if (year == 0) yearError(pos - 2) + year = -year + } + if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) + year + } + } + val month = { + if (pos + 1 >= len) yearMonthError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (month < 1 || month > 12) monthError(pos + 1) + pos += 2 + month + } + if (pos != len) yearMonthError(pos) + YearMonth.of(year, month) + } + + def unsafeParseZonedDateTime(input: String): ZonedDateTime = { + val len = input.length + var pos = 0 + val year = { + if (pos + 4 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + if (ch0 >= '0' && ch0 <= '9') { + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 != '-') charError('-', pos + 4) + pos += 5 + ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch2 < '0' || ch2 > '9') digitError(pos + 2) + if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + pos += 5 + var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + var yearDigits = 4 + var ch: Char = '0' + while ({ + if (pos >= len) zonedDateTimeError(pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + if (yearNeg) { + if (year == 0) yearError(pos - 2) + year = -year + } + if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) + year + } + } + val month = { + if (pos + 2 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (month < 1 || month > 12) monthError(pos + 1) + if (ch2 != '-') charError('-', pos + 2) + pos += 3 + month + } + val day = { + if (pos + 2 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) + if (ch2 != 'T') charError('T', pos + 2) + pos += 3 + day + } + val hour = { + if (pos + 2 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (hour > 23) hourError(pos + 1) + if (ch2 != ':') charError(':', pos + 2) + pos += 3 + hour + } + val minute = { + if (pos + 1 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') minuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + var second, nano = 0 + var nanoDigitWeight = -1 + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + var ch = input.charAt(pos) + pos += 1 + if (ch == ':') { + nanoDigitWeight = -2 + second = { + if (pos + 1 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') secondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + ch = input.charAt(pos) + pos += 1 + if (ch == '.') { + nanoDigitWeight = 100000000 + while ({ + if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + }) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + val localDateTime = LocalDateTime.of(year, month, day, hour, minute, second, nano) + val zoneOffset = + if (ch == 'Z') { + if (pos < len) { + ch = input.charAt(pos) + if (ch != '[') charError('[', pos) + pos += 1 + } + ZoneOffset.UTC + } else { + val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) + nanoDigitWeight = -3 + val offsetHour = { + if (pos + 1 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (offsetHour > 18) timezoneOffsetHourError(pos + 1) + pos += 2 + offsetHour + } + var offsetMinute, offsetSecond = 0 + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' || ch != '[' && charError('[', pos - 1) + } + ) { + offsetMinute = { + if (pos + 1 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' || ch != '[' && charError('[', pos - 1) + } + ) { + nanoDigitWeight = -4 + offsetSecond = { + if (pos + 1 >= len) zonedDateTimeError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetSecondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if (pos < len) { + ch = input.charAt(pos) + if (ch != '[') charError('[', pos) + pos += 1 + } + } + } + toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + } + if (ch == '[') { + val zone = + try { + val from = pos + while ({ + if (pos >= len) zonedDateTimeError(pos) + ch = input.charAt(pos) + ch != ']' + }) pos += 1 + val key = input.substring(from, pos) + var zoneId = zoneIds.get(key) + if ( + (zoneId eq null) && { + zoneId = ZoneId.of(key) + !zoneId.isInstanceOf[ZoneOffset] || zoneId.asInstanceOf[ZoneOffset].getTotalSeconds % 900 == 0 + } + ) zoneIds.put(key, zoneId) + zoneId + } catch { + case _: DateTimeException => zonedDateTimeError(pos - 1) + } + pos += 1 + if (pos != len) zonedDateTimeError(pos) + ZonedDateTime.ofInstant(localDateTime, zoneOffset, zone) + } else ZonedDateTime.ofLocal(localDateTime, zoneOffset, null) + } + + def unsafeParseZoneId(input: String): ZoneId = + try { + var zoneId = zoneIds.get(input) + if ( + (zoneId eq null) && { + zoneId = ZoneId.of(input) + !zoneId.isInstanceOf[ZoneOffset] || zoneId.asInstanceOf[ZoneOffset].getTotalSeconds % 900 == 0 + } + ) zoneIds.put(input, zoneId) + zoneId + } catch { + case _: DateTimeException => error("illegal zone id", 0) + } + + def unsafeParseZoneOffset(input: String): ZoneOffset = { + val len = input.length + var pos, nanoDigitWeight = 0 + if (pos >= len) zoneOffsetError(pos) + var ch = input.charAt(pos) + pos += 1 + if (ch == 'Z') ZoneOffset.UTC + else { + val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) + nanoDigitWeight = -3 + val offsetHour = { + if (pos + 1 >= len) zoneOffsetError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (offsetHour > 18) timezoneOffsetHourError(pos + 1) + pos += 2 + offsetHour + } + var offsetMinute, offsetSecond = 0 + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' + } + ) { + offsetMinute = { + if (pos + 1 >= len) zoneOffsetError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + if ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch == ':' + } + ) { + nanoDigitWeight = -4 + offsetSecond = { + if (pos + 1 >= len) zoneOffsetError(pos) + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + if (ch0 < '0' || ch0 > '9') digitError(pos) + if (ch1 < '0' || ch1 > '9') digitError(pos + 1) + if (ch0 > '5') timezoneOffsetSecondError(pos + 1) + pos += 2 + ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + } + } + } + if (pos != len) zoneOffsetError(pos) + toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + } + } + + private[this] def toZoneOffset( + offsetNeg: Boolean, + offsetHour: Int, + offsetMinute: Int, + offsetSecond: Int, + pos: Int + ): ZoneOffset = { + var offsetTotal = offsetHour * 3600 + offsetMinute * 60 + offsetSecond + var qp = offsetTotal * 37283 + if (offsetTotal > 64800) zoneOffsetError(pos) // 64800 == 18 * 60 * 60 + if ((qp & 0x1ff8000) == 0) { // check if offsetTotal divisible by 900 + qp >>>= 25 // divide offsetTotal by 900 + if (offsetNeg) qp = -qp + var zoneOffset = zoneOffsets(qp + 72) + if (zoneOffset ne null) zoneOffset + else { + if (offsetNeg) offsetTotal = -offsetTotal + zoneOffset = ZoneOffset.ofTotalSeconds(offsetTotal) + zoneOffsets(qp + 72) = zoneOffset + zoneOffset + } + } else { + if (offsetNeg) offsetTotal = -offsetTotal + ZoneOffset.ofTotalSeconds(offsetTotal) + } + } + + private[this] def sumSeconds(s1: Long, s2: Long, pos: Int): Long = { + val s = s1 + s2 + if (((s1 ^ s) & (s2 ^ s)) < 0) durationError(pos) + s + } + + private[this] def epochDayForYear(year: Int): Long = + year * 365L + ((year + 3 >> 2) - { + val cp = year * 1374389535L + if (year < 0) (cp >> 37) - (cp >> 39) // year / 100 - year / 400 + else (cp + 136064563965L >> 37) - (cp + 548381424465L >> 39) // (year + 99) / 100 - (year + 399) / 400 + }.toInt) + + private[this] def dayOfYearForYearMonth(year: Int, month: Int): Int = + (month * 1002277 - 988622 >> 15) - // (month * 367 - 362) / 12 + (if (month <= 2) 0 + else if (isLeap(year)) 1 + else 2) + + private[this] def maxDayForMonth(month: Int): Int = + if (month != 2) ((month >> 3) ^ (month & 0x1)) + 30 + else 29 + + private[this] def maxDayForYearMonth(year: Int, month: Int): Int = + if (month != 2) ((month >> 3) ^ (month & 0x1)) + 30 + else if (isLeap(year)) 29 + else 28 + + private[this] def isLeap(year: Int): Boolean = (year & 0x3) == 0 && { // (year % 100 != 0 || year % 400 == 0) + val cp = year * 1374389535L + val cc = year >> 31 + ((cp ^ cc) & 0x1fc0000000L) != 0 || (((cp >> 37).toInt - cc) & 0x3) == 0 + } + + private[this] def nanoError(nanoDigitWeight: Int, ch: Char, pos: Int): Nothing = { + if (nanoDigitWeight == 0) charError(ch, pos) + charOrDigitError(ch, pos) + } + + private[this] def durationOrPeriodStartError(isNeg: Boolean, pos: Int) = + error( + if (isNeg) "expected 'P'" + else "expected 'P' or '-'", + pos + ) + + private[this] def durationOrPeriodDigitError(isNegX: Boolean, isNumReq: Boolean, pos: Int): Nothing = + error( + if (isNegX) "expected digit" + else if (isNumReq) "expected '-' or digit" + else "expected '\"' or '-' or digit", + pos + ) + + private[this] def durationError(state: Int, pos: Int): Nothing = + error( + (state: @switch) match { + case 0 => "expected 'D' or digit" + case 1 => "expected 'H' or 'M' or 'S or '.' or digit" + case 2 => "expected 'M' or 'S or '.' or digit" + case 3 => "expected 'S or '.' or digit" + }, + pos + ) + + private[this] def durationError(pos: Int) = error("illegal duration", pos) + + private[this] def instantError(nanoDigitWeight: Int, pos: Int) = error( + if (nanoDigitWeight == -1) "expected ':' or 'Z'" + else if (nanoDigitWeight == -2) "expected '.' or 'Z'" + else if (nanoDigitWeight == 0) "expected 'Z'" + else "expected digit or 'Z'", + pos + ) + + private[this] def timezoneSignError(nanoDigitWeight: Int, pos: Int) = + error( + if (nanoDigitWeight == -2) "expected '.' or '+' or '-' or 'Z'" + else if (nanoDigitWeight == -1) "expected ':' or '+' or '-' or 'Z'" + else if (nanoDigitWeight == 0) "expected '+' or '-' or 'Z'" + else "expected digit or '+' or '-' or 'Z'", + pos + ) + + private[this] def instantError(pos: Int) = error("illegal instant", pos) + + private[this] def localDateError(pos: Int) = error("illegal local date", pos) + + private[this] def localDateTimeError(pos: Int) = error("illegal local date time", pos) + + private[this] def localTimeError(pos: Int) = error("illegal local time", pos) + + private[this] def offsetDateTimeError(pos: Int) = error("illegal offset date time", pos) + + private[this] def offsetTimeError(pos: Int) = error("illegal offset time", pos) + + private[this] def periodError(state: Int, pos: Int): Nothing = + error( + (state: @switch) match { + case 0 => "expected 'Y' or 'M' or 'W' or 'D' or digit" + case 1 => "expected 'M' or 'W' or 'D' or digit" + case 2 => "expected 'W' or 'D' or digit" + case 3 => "expected 'D' or digit" + }, + pos + ) + + private[this] def periodError(pos: Int) = error("illegal period", pos) + + private[this] def yearMonthError(pos: Int) = error("illegal year month", pos) + + private[this] def zonedDateTimeError(pos: Int) = error("illegal zoned date time", pos) + + private[this] def zoneOffsetError(pos: Int) = error("illegal zone offset", pos) + + private[this] def yearError(yearNeg: Boolean, yearDigits: Int, pos: Int) = { + if (!yearNeg && yearDigits == 4) digitError(pos) + if (yearDigits == 9) charError('-', pos) + charOrDigitError('-', pos) + } + + private[this] def yearError(pos: Int) = error("illegal year", pos) + + private[this] def monthError(pos: Int) = error("illegal month", pos) + + private[this] def dayError(pos: Int) = error("illegal day", pos) + + private[this] def hourError(pos: Int) = error("illegal hour", pos) + + private[this] def minuteError(pos: Int) = error("illegal minute", pos) + + private[this] def secondError(pos: Int) = error("illegal second", pos) + + private[this] def timezoneOffsetHourError(pos: Int) = error("illegal timezone offset hour", pos) + + private[this] def timezoneOffsetMinuteError(pos: Int) = error("illegal timezone offset minute", pos) + + private[this] def timezoneOffsetSecondError(pos: Int) = error("illegal timezone offset second", pos) + + private[this] def digitError(pos: Int) = error("expected digit", pos) + + private[this] def charsOrDigitError(ch1: Char, ch2: Char, pos: Int) = + error(s"expected '$ch1' or '$ch2' or digit", pos) + + private[this] def charsError(ch1: Char, ch2: Char, pos: Int) = error(s"expected '$ch1' or '$ch2'", pos) + + private[this] def charOrDigitError(ch1: Char, pos: Int) = error(s"expected '$ch1' or digit", pos) + + private[this] def charError(ch: Char, pos: Int) = error(s"expected '$ch'", pos) + + private[this] def error(msg: String, pos: Int) = + throw new DateTimeException(msg + " at index " + pos) with NoStackTrace +} diff --git a/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/javatime/serializers.scala b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/javatime/serializers.scala new file mode 100644 index 00000000..a443e7c2 --- /dev/null +++ b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/javatime/serializers.scala @@ -0,0 +1,253 @@ +package zio.morphir.sexpr.javatime + +import java.time._ + +private[sexpr] object serializers { + def toString(x: Duration): String = { + val s = new java.lang.StringBuilder(16) + s.append('P').append('T') + val totalSecs = x.getSeconds + var nano = x.getNano + if ((totalSecs | nano) == 0) s.append('0').append('S') + else { + var effectiveTotalSecs = totalSecs + if (totalSecs < 0 && nano > 0) effectiveTotalSecs += 1 + val hours = effectiveTotalSecs / 3600 // 3600 == seconds in a hour + val secsOfHour = (effectiveTotalSecs - hours * 3600).toInt + val minutes = secsOfHour / 60 + val seconds = secsOfHour - minutes * 60 + if (hours != 0) s.append(hours).append('H') + if (minutes != 0) s.append(minutes).append('M') + if ((seconds | nano) != 0) { + if (totalSecs < 0 && seconds == 0) s.append('-').append('0') + else s.append(seconds) + if (nano != 0) { + if (totalSecs < 0) nano = 1000000000 - nano + val dotPos = s.length + s.append(nano + 1000000000) + var i = s.length - 1 + while (s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s.setCharAt(dotPos, '.') + } + s.append('S') + } + } + s.toString + } + + def toString(x: Instant): String = { + val s = new java.lang.StringBuilder(32) + val epochSecond = x.getEpochSecond + val epochDay = + (if (epochSecond >= 0) epochSecond + else epochSecond - 86399) / 86400 // 86400 == seconds per day + val secsOfDay = (epochSecond - epochDay * 86400).toInt + var marchZeroDay = epochDay + 719468 // 719468 == 719528 - 60 == days 0000 to 1970 - days 1st Jan to 1st Mar + var adjustYear = 0 + if (marchZeroDay < 0) { // adjust negative years to positive for calculation + val adjust400YearCycles = to400YearCycle(marchZeroDay + 1) - 1 + adjustYear = adjust400YearCycles * 400 + marchZeroDay -= adjust400YearCycles * 146097L + } + var year = to400YearCycle(marchZeroDay * 400 + 591) + var marchDayOfYear = toMarchDayOfYear(marchZeroDay, year) + if (marchDayOfYear < 0) { // fix year estimate + year -= 1 + marchDayOfYear = toMarchDayOfYear(marchZeroDay, year) + } + val marchMonth = (marchDayOfYear * 17135 + 6854) >> 19 // (marchDayOfYear * 5 + 2) / 153 + year += (marchMonth * 3277 >> 15) + adjustYear // year += marchMonth / 10 + adjustYear (reset any negative year and convert march-based values back to january-based) + val month = marchMonth + + (if (marchMonth < 10) 3 + else -9) + val day = + marchDayOfYear - ((marchMonth * 1002762 - 16383) >> 15) // marchDayOfYear - (marchMonth * 306 + 5) / 10 + 1 + val hour = secsOfDay * 37283 >>> 27 // divide a small positive int by 3600 + val secsOfHour = secsOfDay - hour * 3600 + val minute = secsOfHour * 17477 >> 20 // divide a small positive int by 60 + val second = secsOfHour - minute * 60 + appendYear(year, s) + append2Digits(month, s.append('-')) + append2Digits(day, s.append('-')) + append2Digits(hour, s.append('T')) + append2Digits(minute, s.append(':')) + append2Digits(second, s.append(':')) + val nano = x.getNano + if (nano != 0) { + s.append('.') + val q1 = nano / 1000000 + val r1 = nano - q1 * 1000000 + append3Digits(q1, s) + if (r1 != 0) { + val q2 = r1 / 1000 + val r2 = r1 - q2 * 1000 + append3Digits(q2, s) + if (r2 != 0) append3Digits(r2, s) + } + } + s.append('Z').toString + } + + def toString(x: LocalDate): String = { + val s = new java.lang.StringBuilder(16) + appendLocalDate(x, s) + s.toString + } + + def toString(x: LocalDateTime): String = { + val s = new java.lang.StringBuilder(32) + appendLocalDate(x.toLocalDate, s) + appendLocalTime(x.toLocalTime, s.append('T')) + s.toString + } + + def toString(x: LocalTime): String = { + val s = new java.lang.StringBuilder(24) + appendLocalTime(x, s) + s.toString + } + + def toString(x: MonthDay): String = { + val s = new java.lang.StringBuilder(8) + append2Digits(x.getMonthValue, s.append('-').append('-')) + append2Digits(x.getDayOfMonth, s.append('-')) + s.toString + } + + def toString(x: OffsetDateTime): String = { + val s = new java.lang.StringBuilder(48) + appendLocalDate(x.toLocalDate, s) + appendLocalTime(x.toLocalTime, s.append('T')) + appendZoneOffset(x.getOffset, s) + s.toString + } + + def toString(x: OffsetTime): String = { + val s = new java.lang.StringBuilder(32) + appendLocalTime(x.toLocalTime, s) + appendZoneOffset(x.getOffset, s) + s.toString + } + + def toString(x: Period): String = { + val s = new java.lang.StringBuilder(16) + s.append('P') + if (x.isZero) s.append('0').append('D') + else { + val years = x.getYears + val months = x.getMonths + val days = x.getDays + if (years != 0) s.append(years).append('Y') + if (months != 0) s.append(months).append('M') + if (days != 0) s.append(days).append('D') + } + s.toString + } + + def toString(x: Year): String = { + val s = new java.lang.StringBuilder(16) + appendYear(x.getValue, s) + s.toString + } + + def toString(x: YearMonth): String = { + val s = new java.lang.StringBuilder(16) + appendYear(x.getYear, s) + append2Digits(x.getMonthValue, s.append('-')) + s.toString + } + + def toString(x: ZonedDateTime): String = { + val s = new java.lang.StringBuilder(48) + appendLocalDate(x.toLocalDate, s) + appendLocalTime(x.toLocalTime, s.append('T')) + appendZoneOffset(x.getOffset, s) + val zone = x.getZone + if (!zone.isInstanceOf[ZoneOffset]) s.append('[').append(zone.getId).append(']') + s.toString + } + + def toString(x: ZoneId): String = x.getId + + def toString(x: ZoneOffset): String = { + val s = new java.lang.StringBuilder(16) + appendZoneOffset(x, s) + s.toString + } + + private[this] def appendLocalDate(x: LocalDate, s: java.lang.StringBuilder): Unit = { + appendYear(x.getYear, s) + append2Digits(x.getMonthValue, s.append('-')) + append2Digits(x.getDayOfMonth, s.append('-')) + } + + private[this] def appendLocalTime(x: LocalTime, s: java.lang.StringBuilder): Unit = { + append2Digits(x.getHour, s) + append2Digits(x.getMinute, s.append(':')) + append2Digits(x.getSecond, s.append(':')) + val nano = x.getNano + if (nano != 0) { + val dotPos = s.length + s.append(nano + 1000000000) + var i = s.length - 1 + while (s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s.setCharAt(dotPos, '.') + } + } + + private[this] def appendZoneOffset(x: ZoneOffset, s: java.lang.StringBuilder): Unit = { + val totalSeconds = x.getTotalSeconds + if (totalSeconds == 0) s.append('Z'): Unit + else { + val q0 = + if (totalSeconds > 0) { + s.append('+') + totalSeconds + } else { + s.append('-') + -totalSeconds + } + val q1 = q0 * 37283 >>> 27 // divide a small positive int by 3600 + val r1 = q0 - q1 * 3600 + append2Digits(q1, s) + s.append(':') + val q2 = r1 * 17477 >> 20 // divide a small positive int by 60 + val r2 = r1 - q2 * 60 + append2Digits(q2, s) + if (r2 != 0) append2Digits(r2, s.append(':')) + } + } + + private[this] def appendYear(x: Int, s: java.lang.StringBuilder): Unit = + if (x >= 0) { + if (x < 10000) append4Digits(x, s) + else s.append('+').append(x): Unit + } else if (x > -10000) append4Digits(-x, s.append('-')) + else s.append(x): Unit + + private[this] def append4Digits(x: Int, s: java.lang.StringBuilder): Unit = { + val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 + append2Digits(q, s) + append2Digits(x - q * 100, s) + } + + private[this] def append3Digits(x: Int, s: java.lang.StringBuilder): Unit = { + val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 + append2Digits(x - q * 100, s.append((q + '0').toChar)) + } + + private[this] def append2Digits(x: Int, s: java.lang.StringBuilder): Unit = { + val q = x * 103 >> 10 // divide a 2-digit positive int by 10 + s.append((q + '0').toChar).append((x + '0' - q * 10).toChar): Unit + } + + private[this] def to400YearCycle(day: Long): Int = + (day / 146097).toInt // 146097 == number of days in a 400 year cycle + + private[this] def toMarchDayOfYear(marchZeroDay: Long, year: Int): Int = { + val century = year / 100 + (marchZeroDay - year * 365L).toInt - (year >> 2) + century - (century >> 2) + } +} diff --git a/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/uuid/UUIDParser.scala b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/uuid/UUIDParser.scala new file mode 100644 index 00000000..0fb8c5ce --- /dev/null +++ b/zio-morphir-sexpr/shared/src/main/scala/zio/morphir/sexpr/uuid/UUIDParser.scala @@ -0,0 +1,121 @@ +package zio.morphir.sexpr.uuid + +import scala.annotation.nowarn + +// A port of https://github.com/openjdk/jdk/commit/ebadfaeb2e1cc7b5ce5f101cd8a539bc5478cf5b with optimizations applied +private[sexpr] object UUIDParser { + // Converts characters to their numeric representation (for example 'E' or 'e' becomes 0XE) + private[this] val CharToNumeric: Array[Byte] = { + // by filling in -1's we prevent from trying to parse invalid characters + val ns = Array.fill[Byte](256)(-1) + + ns('0') = 0 + ns('1') = 1 + ns('2') = 2 + ns('3') = 3 + ns('4') = 4 + ns('5') = 5 + ns('6') = 6 + ns('7') = 7 + ns('8') = 8 + ns('9') = 9 + + ns('A') = 10 + ns('B') = 11 + ns('C') = 12 + ns('D') = 13 + ns('E') = 14 + ns('F') = 15 + + ns('a') = 10 + ns('b') = 11 + ns('c') = 12 + ns('d') = 13 + ns('e') = 14 + ns('f') = 15 + + ns + } + + def unsafeParse(input: String): java.util.UUID = + if ( + input.length != 36 || { + val ch1 = input.charAt(8) + val ch2 = input.charAt(13) + val ch3 = input.charAt(18) + val ch4 = input.charAt(23) + ch1 != '-' || ch2 != '-' || ch3 != '-' || ch4 != '-' + } + ) unsafeParseExtended(input) + else { + val ch2n = CharToNumeric + val msb1 = parseNibbles(ch2n, input, 0) + val msb2 = parseNibbles(ch2n, input, 4) + val msb3 = parseNibbles(ch2n, input, 9) + val msb4 = parseNibbles(ch2n, input, 14) + val lsb1 = parseNibbles(ch2n, input, 19) + val lsb2 = parseNibbles(ch2n, input, 24) + val lsb3 = parseNibbles(ch2n, input, 28) + val lsb4 = parseNibbles(ch2n, input, 32) + if ((msb1 | msb2 | msb3 | msb4 | lsb1 | lsb2 | lsb3 | lsb4) < 0) invalidUUIDError(input) + new java.util.UUID(msb1 << 48 | msb2 << 32 | msb3 << 16 | msb4, lsb1 << 48 | lsb2 << 32 | lsb3 << 16 | lsb4) + } + + // A nibble is 4 bits + @nowarn("msg=implicit numeric widening") + private[this] def parseNibbles(ch2n: Array[Byte], input: String, position: Int): Long = { + val ch1 = input.charAt(position) + val ch2 = input.charAt(position + 1) + val ch3 = input.charAt(position + 2) + val ch4 = input.charAt(position + 3) + if ((ch1 | ch2 | ch3 | ch4) > 0xff) -1L + else ch2n(ch1) << 12 | ch2n(ch2) << 8 | ch2n(ch3) << 4 | ch2n(ch4) + } + + private[this] def unsafeParseExtended(input: String): java.util.UUID = { + val len = input.length + if (len > 36) throw new IllegalArgumentException("UUID string too large") + val dash1 = input.indexOf('-', 0) + val dash2 = input.indexOf('-', dash1 + 1) + val dash3 = input.indexOf('-', dash2 + 1) + val dash4 = input.indexOf('-', dash3 + 1) + val dash5 = input.indexOf('-', dash4 + 1) + + // For any valid input, dash1 through dash4 will be positive and dash5 will be negative, + // but it's enough to check dash4 and dash5: + // - if dash1 is -1, dash4 will be -1 + // - if dash1 is positive but dash2 is -1, dash4 will be -1 + // - if dash1 and dash2 is positive, dash3 will be -1, dash4 will be positive, but so will dash5 + if (dash4 < 0 || dash5 >= 0) invalidUUIDError(input) + + val ch2n = CharToNumeric + val section1 = parseSection(ch2n, input, 0, dash1, 0xfffffff00000000L) + val section2 = parseSection(ch2n, input, dash1 + 1, dash2, 0xfffffffffff0000L) + val section3 = parseSection(ch2n, input, dash2 + 1, dash3, 0xfffffffffff0000L) + val section4 = parseSection(ch2n, input, dash3 + 1, dash4, 0xfffffffffff0000L) + val section5 = parseSection(ch2n, input, dash4 + 1, len, 0xfff000000000000L) + new java.util.UUID((section1 << 32) | (section2 << 16) | section3, (section4 << 48) | section5) + } + + @nowarn("msg=implicit numeric widening") + private[this] def parseSection( + ch2n: Array[Byte], + input: String, + beginIndex: Int, + endIndex: Int, + zeroMask: Long + ): Long = { + if (beginIndex >= endIndex || beginIndex + 16 < endIndex) invalidUUIDError(input) + var result = 0L + var i = beginIndex + while (i < endIndex) { + result = (result << 4) | ch2n(input.charAt(i)) + i += 1 + } + if ((result & zeroMask) != 0) invalidUUIDError(input) + result + } + + private[this] def invalidUUIDError(input: String): IllegalArgumentException = + throw new IllegalArgumentException(input) +} diff --git a/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/DecoderSpec.scala b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/DecoderSpec.scala index 39692f14..78b5c22a 100644 --- a/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/DecoderSpec.scala +++ b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/DecoderSpec.scala @@ -1,9 +1,16 @@ package zio.morphir.sexpr +import zio._ +import zio.morphir.sexpr.ast._ import zio.morphir.testing.ZioBaseSpec -import zio.test.TestAspect.{ignore, tag} +import zio.test.Assertion._ import zio.test._ +import java.time._ +import java.util.UUID +import java.time.LocalTime +import scala.collection.immutable + object DecoderSpec extends ZioBaseSpec { def spec = suite("Decoder")( suite("fromSExpr")( @@ -16,10 +23,354 @@ object DecoderSpec extends ZioBaseSpec { "\"hello\\u0000world\"".fromSExpr[String] == Right("hello\u0000world") ) // "hello\u0000world".toSExpr == "\"hello\\u0000world\"" - } + test("boolean") { + }, + testM("bigInt") { + check(Gens.genBigInteger) { x => + assertTrue(x.toString.fromSExpr[java.math.BigInteger] == Right(x)) + } + }, + testM("bigDecimal") { + check(Gens.genBigDecimal) { x => + assertTrue(x.toString.fromSExpr[java.math.BigDecimal] == Right(x)) + } + }, + test("boolean") { assertTrue("true".fromSExpr[Boolean] == Right(true)) && assertTrue("false".fromSExpr[Boolean] == Right(false)) - } @@ ignore @@ tag("Something isn't working right!") + }, // @@ ignore @@ tag("Something isn't working right!"), + testM("byte") { + check(Gen.anyByte) { x => + assertTrue(x.toString.fromSExpr[Byte] == Right(x)) + } + }, + testM("char2") { + check(Gen.anyChar) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[Char] == Right(x)) + } + }, + testM("double") { + check(Gen.anyDouble) { x => + assertTrue(x.toString.fromSExpr[Double] == Right(x)) + } + }, + testM("float") { + check(Gen.anyFloat) { x => + assertTrue(x.toString.fromSExpr[Float] == Right(x)) + } + }, + testM("int") { + check(Gen.anyInt) { x => + assertTrue(x.toString.fromSExpr[Int] == Right(x)) + } + }, + testM("long") { + check(Gen.anyLong) { x => + assertTrue(x.toString.fromSExpr[Long] == Right(x)) + } + }, + testM("short") { + check(Gen.anyShort) { x => + assertTrue(x.toString.fromSExpr[Short] == Right(x)) + } + }, + suite("java.util.UUID")( + testM("Auto-generated") { + check(Gen.anyUUID) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[UUID] == Right(x)) + } + }, + test("Manual") { + val ok1 = """"64d7c38d-2afd-4514-9832-4e70afe4b0f8"""" + val ok2 = """"0000000064D7C38D-FD-14-32-70AFE4B0f8"""" + val ok3 = """"0-0-0-0-0"""" + val bad1 = """""""" + val bad2 = """"64d7c38d-2afd-4514-9832-4e70afe4b0f80"""" + val bad3 = """"64d7c38d-2afd-4514-983-4e70afe4b0f80"""" + val bad4 = """"64d7c38d-2afd--9832-4e70afe4b0f8"""" + val bad5 = """"64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"""" + val bad6 = """"64d7c38d-2afd-X-9832-4e70afe4b0f8"""" + val bad7 = """"0-0-0-0-00000000000000000"""" + + assert(ok1.fromSExpr[UUID])(isRight(equalTo(UUID.fromString("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && + assert(ok2.fromSExpr[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AfE4B0f8")))) && + assert(ok3.fromSExpr[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && + assert(bad1.fromSExpr[UUID])(isLeft(containsString("Invalid UUID: "))) && + assert(bad2.fromSExpr[UUID])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad3.fromSExpr[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && + assert(bad4.fromSExpr[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && + assert(bad5.fromSExpr[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && + assert(bad6.fromSExpr[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8"))) && + assert(bad7.fromSExpr[UUID])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + } + ) + ), + suite("java.time")( + suite("Duration")( + testM("Auto-generated") { + check(Gens.genDuration) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[Duration] == Right(x)) + } + }, + test("Manual") { + val ok1 = """"PT1H2M3S"""" + val ok2 = """"PT-0.5S"""" // see https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8054978 + val bad1 = """"PT-H"""" + + assert(ok1.fromSExpr[Duration])(isRight(equalTo(Duration.parse("PT1H2M3S")))) && + assert(ok2.fromSExpr[Duration])(isRight(equalTo(Duration.ofNanos(-500000000)))) && + assert(bad1.fromSExpr[Duration])( + isLeft(containsString("PT-H is not a valid ISO-8601 format, expected digit at index 3")) + ) + } + ), + testM("Instant") { + check(Gens.genInstant) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[Instant] == Right(x)) + } + }, + testM("LocalDate") { + check(Gens.genLocalDate) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[LocalDate] == Right(x)) + } + }, + testM("LocalDateTime") { + check(Gens.genLocalDateTime) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[LocalDateTime] == Right(x)) + } + }, + testM("LocalTime") { + check(Gens.genLocalTime) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[LocalTime] == Right(x)) + } + }, + testM("Month") { + check(Gens.genMonth) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[Month] == Right(x)) + } + }, + testM("MonthDay") { + check(Gens.genMonthDay) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[MonthDay] == Right(x)) + } + }, + testM("OffsetDateTime") { + check(Gens.genOffsetDateTime) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[OffsetDateTime] == Right(x)) + } + }, + testM("OffsetTime") { + check(Gens.genOffsetTime) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[OffsetTime] == Right(x)) + } + }, + testM("Period") { + check(Gens.genPeriod) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[Period] == Right(x)) + } + }, + testM("Year") { + check(Gens.genYear) { x => + val year = "%04d".format(x.getValue) + assertTrue(s"\"$year\"".fromSExpr[Year] == Right(x)) + } + }, + testM("YearMonth") { + check(Gens.genYearMonth) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[YearMonth] == Right(x)) + } + }, + testM("ZoneId") { + check(Gens.genZoneId) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[ZoneId] == Right(x)) + } + }, + testM("ZoneOffset") { + check(Gens.genZoneOffset) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[ZoneOffset] == Right(x)) + } + }, + suite("ZonedDateTime")( + testM("Auto-generated") { + check(Gens.genZonedDateTime) { x => + assertTrue(s"\"${x.toString}\"".fromSExpr[ZonedDateTime] == Right(x)) + } + }, + test("Manual") { + val ok1 = """"2021-06-20T20:03:51.533418+02:00[Europe/Warsaw]"""" + val ok2 = + """"2018-10-28T02:30+00:00[Europe/Warsaw]"""" // see https://bugs.openjdk.java.net/browse/JDK-8066982 + val bad1 = """"2018-10-28T02:30"""" + + assert(ok1.fromSExpr[ZonedDateTime])( + isRight(equalTo(ZonedDateTime.parse("2021-06-20T20:03:51.533418+02:00[Europe/Warsaw]"))) + ) && + assert(ok2.fromSExpr[ZonedDateTime].map(_.toOffsetDateTime))( + isRight(equalTo(OffsetDateTime.parse("2018-10-28T03:30+01:00"))) + ) && + assert(bad1.fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2018-10-28T02:30 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" + ) + ) + ) + } + ) + ), + suite("Collections")( + test("Seq") { + val sexprStr = """["5XL","2XL","XL"]""" + val expected = Seq("5XL", "2XL", "XL") + + assert(sexprStr.fromSExpr[Seq[String]])(isRight(equalTo(expected))) + }, + test("Vector") { + val sexprStr = """["5XL","2XL","XL"]""" + val expected = Vector("5XL", "2XL", "XL") + + assert(sexprStr.fromSExpr[Vector[String]])(isRight(equalTo(expected))) + }, + test("SortedSet") { + val sexprStr = """["5XL","2XL","XL"]""" + val expected = immutable.SortedSet("5XL", "2XL", "XL") + + assert(sexprStr.fromSExpr[immutable.SortedSet[String]])(isRight(equalTo(expected))) + }, + test("HashSet") { + val sexprStr = """["5XL","2XL","XL"]""" + val expected = immutable.HashSet("5XL", "2XL", "XL") + + assert(sexprStr.fromSExpr[immutable.HashSet[String]])(isRight(equalTo(expected))) + }, + test("Set") { + val sexprStr = """["5XL","2XL","XL"]""" + val expected = Set("5XL", "2XL", "XL") + + assert(sexprStr.fromSExpr[Set[String]])(isRight(equalTo(expected))) + }, + test("zio.Chunk") { + val sexprStr = """["5XL","2XL","XL"]""" + val expected = Chunk("5XL", "2XL", "XL") + + assert(sexprStr.fromSExpr[Chunk[String]])(isRight(equalTo(expected))) + }, + test("zio.NonEmptyChunk") { + val sexprStr = """["5XL","2XL","XL"]""" + val expected = NonEmptyChunk("5XL", "2XL", "XL") + + assert(sexprStr.fromSExpr[NonEmptyChunk[String]])(isRight(equalTo(expected))) + }, + test("zio.NonEmptyChunk failure") { + val sexprStr = "[]" + + assert(sexprStr.fromSExpr[NonEmptyChunk[String]])(isLeft(equalTo("(Chunk was empty)"))) + }, + test("collections") { + val arr = """[1, 2, 3]""" + + assert(arr.fromSExpr[Array[Int]])(isRight(equalTo(Array(1, 2, 3)))) && + assert(arr.fromSExpr[IndexedSeq[Int]])(isRight(equalTo(IndexedSeq(1, 2, 3)))) && + assert(arr.fromSExpr[immutable.LinearSeq[Int]])(isRight(equalTo(immutable.LinearSeq(1, 2, 3)))) && + assert(arr.fromSExpr[immutable.ListSet[Int]])(isRight(equalTo(immutable.ListSet(1, 2, 3)))) && + assert(arr.fromSExpr[immutable.TreeSet[Int]])(isRight(equalTo(immutable.TreeSet(1, 2, 3)))) + } + ), + suite("fromAST")( + testM("BigDecimal") { + check(Gens.genBigDecimal) { x => + assert(SExpr.Num(x).as[java.math.BigDecimal])(isRight(equalTo(x))) + } + }, + // TODO need encoders for these + // test("Seq") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = Seq("5XL", "2XL", "XL") + + // assert(sexpr.as[Seq[String]])(isRight(equalTo(expected))) + // }, + // test("IndexedSeq") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = IndexedSeq("5XL", "2XL", "XL") + + // assert(sexpr.as[IndexedSeq[String]])(isRight(equalTo(expected))) + // }, + // test("LinearSeq") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = immutable.LinearSeq("5XL", "2XL", "XL") + + // assert(sexpr.as[immutable.LinearSeq[String]])(isRight(equalTo(expected))) + // }, + // test("ListSet") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = immutable.ListSet("5XL", "2XL", "XL") + + // assert(sexpr.as[immutable.ListSet[String]])(isRight(equalTo(expected))) + // }, + // test("TreeSet") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = immutable.TreeSet("5XL", "2XL", "XL") + + // assert(sexpr.as[immutable.TreeSet[String]])(isRight(equalTo(expected))) + // }, + // test("Vector") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = Vector("5XL", "2XL", "XL") + + // assert(sexpr.as[Vector[String]])(isRight(equalTo(expected))) + // }, + // test("SortedSet") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = immutable.SortedSet("5XL", "2XL", "XL") + + // assert(sexpr.as[immutable.SortedSet[String]])(isRight(equalTo(expected))) + // }, + // test("HashSet") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = immutable.HashSet("5XL", "2XL", "XL") + + // assert(sexpr.as[immutable.HashSet[String]])(isRight(equalTo(expected))) + // }, + // test("Set") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = Set("5XL", "2XL", "XL") + + // assert(sexpr.as[Set[String]])(isRight(equalTo(expected))) + // }, + // test("zio.Chunk") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = Chunk("5XL", "2XL", "XL") + + // assert(sexpr.as[Chunk[String]])(isRight(equalTo(expected))) + // }, + // test("zio.NonEmptyChunk") { + // val sexpr = SExpr.vector(SExpr.Str("5XL"), SExpr.Str("2XL"), SExpr.Str("XL")) + // val expected = NonEmptyChunk("5XL", "2XL", "XL") + + // assert(sexpr.as[NonEmptyChunk[String]])(isRight(equalTo(expected))) + // }, + test("java.util.UUID") { + val ok1 = SExpr.Str("64d7c38d-2afd-4514-9832-4e70afe4b0f8") + val ok2 = SExpr.Str("0000000064D7C38D-FD-14-32-70AFE4B0f8") + val ok3 = SExpr.Str("0-0-0-0-0") + val bad1 = SExpr.Str("") + val bad2 = SExpr.Str("64d7c38d-2afd-4514-9832-4e70afe4b0f80") + val bad3 = SExpr.Str("64d7c38d-2afd-4514-983-4e70afe4b0f80") + val bad4 = SExpr.Str("64d7c38d-2afd--9832-4e70afe4b0f8") + val bad5 = SExpr.Str("64d7c38d-2afd-XXXX-9832-4e70afe4b0f8") + val bad6 = SExpr.Str("64d7c38d-2afd-X-9832-4e70afe4b0f8") + val bad7 = SExpr.Str("0-0-0-0-00000000000000000") + + assert(ok1.as[UUID])(isRight(equalTo(UUID.fromString("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && + assert(ok2.as[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AFE4B0f8")))) && + assert(ok3.as[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && + assert(bad1.as[UUID])(isLeft(containsString("Invalid UUID: "))) && + assert(bad2.as[UUID])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad3.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && + assert(bad4.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && + assert(bad5.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && + assert(bad6.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8"))) && + assert(bad7.as[UUID])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + } ) ) ) diff --git a/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/Gens.scala b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/Gens.scala new file mode 100644 index 00000000..046b4523 --- /dev/null +++ b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/Gens.scala @@ -0,0 +1,117 @@ +package zio.morphir.sexpr + +import zio.random.Random +import zio.test.{Gen, Sized} + +import java.math.BigInteger +import java.time._ +import scala.jdk.CollectionConverters._ +import scala.util.Try + +object Gens { + val genBigInteger: Gen[Random, BigInteger] = + Gen + .bigInt((BigInt(2).pow(128) - 1) * -1, BigInt(2).pow(128) - 1) + .map(_.bigInteger) + .filter(_.bitLength < 128) + + val genBigDecimal: Gen[Random, java.math.BigDecimal] = + Gen + .bigDecimal((BigDecimal(2).pow(128) - 1) * -1, BigDecimal(2).pow(128) - 1) + .map(_.bigDecimal) + .filter(_.toBigInteger.bitLength < 128) + + val genUsAsciiString: Gen[Random with Sized, String] = + Gen.string(Gen.oneOf(Gen.char('!', '~'))) + + val genAlphaLowerString: Gen[Random with Sized, String] = + Gen.string(Gen.oneOf(Gen.char('a', 'z'))) + + // Needs to be an ISO-8601 year between 0000 and 9999 + val anyIntYear: Gen[Random, Int] = Gen.int(0, 9999) + + val genYear: Gen[Random, Year] = anyIntYear.map(Year.of) + + val genLocalDate: Gen[Random, LocalDate] = for { + year <- genYear + month <- Gen.int(1, 12) + day <- Gen.int(1, Month.of(month).length(year.isLeap)) + } yield LocalDate.of(year.getValue, month, day) + + val genLocalTime: Gen[Random, LocalTime] = for { + hour <- Gen.int(0, 23) + minute <- Gen.int(0, 59) + second <- Gen.int(0, 59) + nano <- Gen.int(0, 999999999) + } yield LocalTime.of(hour, minute, second, nano) + + val genInstant: Gen[Random, Instant] = for { + epochSecond <- Gen.long(Instant.MIN.getEpochSecond, Instant.MAX.getEpochSecond) + nanoAdjustment <- Gen.long(Long.MinValue, Long.MaxValue) + fallbackInstant <- Gen.elements(Instant.MIN, Instant.EPOCH, Instant.MAX) + } yield Try(Instant.ofEpochSecond(epochSecond, nanoAdjustment)).getOrElse(fallbackInstant) + + val genZoneOffset: Gen[Random, ZoneOffset] = Gen.oneOf( + Gen.int(-18, 18).map(ZoneOffset.ofHours), + Gen.int(-18 * 60, 18 * 60).map(x => ZoneOffset.ofHoursMinutes(x / 60, x % 60)), + Gen.int(-18 * 60 * 60, 18 * 60 * 60).map(ZoneOffset.ofTotalSeconds) + ) + + val genZoneId: Gen[Random, ZoneId] = Gen.oneOf( + genZoneOffset, + genZoneOffset.map(zo => ZoneId.ofOffset("UT", zo)), + genZoneOffset.map(zo => ZoneId.ofOffset("UTC", zo)), + genZoneOffset.map(zo => ZoneId.ofOffset("GMT", zo)), + Gen.elements(ZoneId.getAvailableZoneIds.asScala.toSeq: _*).map(ZoneId.of), + Gen.elements(ZoneId.SHORT_IDS.values().asScala.toSeq: _*).map(ZoneId.of) + ) + + val genLocalDateTime: Gen[Random, LocalDateTime] = for { + localDate <- genLocalDate + localTime <- genLocalTime + } yield LocalDateTime.of(localDate, localTime) + + val genZonedDateTime: Gen[Random, ZonedDateTime] = for { + localDateTime <- genLocalDateTime + zoneId <- genZoneId + } yield ZonedDateTime.of(localDateTime, zoneId) + + val genDuration: Gen[Random, Duration] = Gen.oneOf( + Gen.long(Long.MinValue / 86400, Long.MaxValue / 86400).map(Duration.ofDays), + Gen.long(Long.MinValue / 3600, Long.MaxValue / 3600).map(Duration.ofHours), + Gen.long(Long.MinValue / 60, Long.MaxValue / 60).map(Duration.ofMinutes), + Gen.long(Long.MinValue, Long.MaxValue).map(Duration.ofSeconds), + Gen.long(Int.MinValue, Int.MaxValue.toLong).map(Duration.ofMillis), + Gen.long(Int.MinValue, Int.MaxValue.toLong).map(Duration.ofNanos) + ) + + val genMonthDay: Gen[Random, MonthDay] = for { + month <- Gen.int(1, 12) + day <- Gen.int(1, 29) + } yield MonthDay.of(month, day) + + val genOffsetDateTime: Gen[Random, OffsetDateTime] = for { + localDateTime <- genLocalDateTime + zoneOffset <- genZoneOffset + } yield OffsetDateTime.of(localDateTime, zoneOffset) + + val genOffsetTime: Gen[Random, OffsetTime] = for { + localTime <- genLocalTime + zoneOffset <- genZoneOffset + } yield OffsetTime.of(localTime, zoneOffset) + + val genPeriod: Gen[Random, Period] = for { + year <- Gen.anyInt + month <- Gen.anyInt + day <- Gen.anyInt + } yield Period.of(year, month, day) + + val genYearMonth: Gen[Random, YearMonth] = for { + year <- genYear + month <- Gen.int(1, 12) + } yield YearMonth.of(year.getValue, month) + + val genDayOfWeek: Gen[Random, DayOfWeek] = Gen.int(1, 7).map(DayOfWeek.of) + + val genMonth: Gen[Random, Month] = Gen.int(1, 12).map(Month.of) +} diff --git a/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/JavaTimeSpec.scala b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/JavaTimeSpec.scala new file mode 100644 index 00000000..9eac8d4b --- /dev/null +++ b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/JavaTimeSpec.scala @@ -0,0 +1,3053 @@ +package zio.morphir.sexpr + +import zio.test.Assertion._ +import zio.test._ +import zio.morphir.testing.ZioBaseSpec + +import java.time._ +import java.time.format.DateTimeFormatter + +object JavaTimeSpec extends ZioBaseSpec { + private def stringify(s: Any): String = s""""${s.toString}"""" + + def spec: Spec[Annotations, TestFailure[Any], TestSuccess] = + suite("java.time")( + suite("Encoder")( + test("DayOfWeek") { + assertTrue( + DayOfWeek.MONDAY.toSExpr == stringify("MONDAY"), + DayOfWeek.TUESDAY.toSExpr == stringify("TUESDAY"), + DayOfWeek.WEDNESDAY.toSExpr == stringify("WEDNESDAY"), + DayOfWeek.THURSDAY.toSExpr == stringify("THURSDAY"), + DayOfWeek.FRIDAY.toSExpr == stringify("FRIDAY"), + DayOfWeek.SATURDAY.toSExpr == stringify("SATURDAY"), + DayOfWeek.SUNDAY.toSExpr == stringify("SUNDAY") + ) + }, + test("Duration") { + assertTrue( + Duration.ofDays(0).toSExpr == stringify("PT0S"), + Duration.ofDays(1).toSExpr == stringify("PT24H"), + Duration.ofHours(24).toSExpr == stringify("PT24H"), + Duration.ofMinutes(1440).toSExpr == stringify("PT24H"), + Duration.ofSeconds(Long.MaxValue, 999999999L).toSExpr == stringify("PT2562047788015215H30M7.999999999S") + ) + // todo uncomment when decoder ready +// // && +// // assert(""""PT-0.5S"""".fromSExpr[Duration].map(_.toString))(isRight(equalTo("PT-0.5S"))) + // assert(""""-PT0.5S"""".fromSExpr[Duration].map(_.toString))(isRight(equalTo("PT-0.5S"))) + }, + test("Instant") { + val n = Instant.now() + assertTrue(Instant.EPOCH.toSExpr == stringify("1970-01-01T00:00:00Z"), n.toSExpr == stringify(n.toString)) + }, + test("LocalDate") { + val n = LocalDate.now() + val p = LocalDate.of(2020, 1, 1) + + assertTrue( + n.toSExpr == stringify(n.format(DateTimeFormatter.ISO_LOCAL_DATE)), + p.toSExpr == stringify("2020-01-01") + ) + }, + test("LocalDateTime") { + val n = LocalDateTime.now() + val p = LocalDateTime.of(2020, 1, 1, 12, 36, 0) + + assertTrue( + n.toSExpr == stringify(n.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)), + p.toSExpr == stringify("2020-01-01T12:36:00") + ) + }, + test("LocalTime") { + val n = LocalTime.now() + val p = LocalTime.of(12, 36, 0) + + assertTrue( + n.toSExpr == stringify(n.format(DateTimeFormatter.ISO_LOCAL_TIME)), + p.toSExpr == stringify("12:36:00") + ) + }, + test("Month") { + assertTrue( + Month.JANUARY.toSExpr == stringify("JANUARY"), + Month.FEBRUARY.toSExpr == stringify("FEBRUARY"), + Month.MARCH.toSExpr == stringify("MARCH"), + Month.APRIL.toSExpr == stringify("APRIL"), + Month.MAY.toSExpr == stringify("MAY"), + Month.JUNE.toSExpr == stringify("JUNE"), + Month.JULY.toSExpr == stringify("JULY"), + Month.AUGUST.toSExpr == stringify("AUGUST"), + Month.SEPTEMBER.toSExpr == stringify("SEPTEMBER"), + Month.OCTOBER.toSExpr == stringify("OCTOBER"), + Month.NOVEMBER.toSExpr == stringify("NOVEMBER"), + Month.DECEMBER.toSExpr == stringify("DECEMBER") + ) + }, + test("MonthDay") { + val n = MonthDay.now() + val p = MonthDay.of(1, 1) + + assertTrue(n.toSExpr == stringify(n.toString), p.toSExpr == stringify("--01-01")) + }, + test("OffsetDateTime") { + val n = OffsetDateTime.now() + val p = OffsetDateTime.of(2020, 1, 1, 12, 36, 12, 0, ZoneOffset.UTC) + + assertTrue( + n.toSExpr == stringify(n.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)), + p.toSExpr == stringify("2020-01-01T12:36:12Z") + ) + }, + test("OffsetTime") { + val n = OffsetTime.now() + val p = OffsetTime.of(12, 36, 12, 0, ZoneOffset.ofHours(-4)) + + assertTrue( + n.toSExpr == stringify(n.format(DateTimeFormatter.ISO_OFFSET_TIME)), + p.toSExpr == stringify("12:36:12-04:00") + ) + }, + test("Period") { + assertTrue( + Period.ZERO.toSExpr == stringify("P0D"), + Period.ofDays(1).toSExpr == stringify("P1D"), + Period.ofMonths(2).toSExpr == stringify("P2M"), + Period.ofWeeks(52).toSExpr == stringify("P364D"), + Period.ofYears(10).toSExpr == stringify("P10Y") + ) + }, + test("Year") { + val n = Year.now() + assertTrue( + n.toSExpr == stringify(n.toString), + Year.of(1999).toSExpr == stringify("1999"), + Year.of(10000).toSExpr == stringify("+10000") + ) + }, + test("YearMonth") { + val n = YearMonth.now() + assertTrue( + n.toSExpr == stringify(n.toString), + YearMonth.of(1999, 12).toSExpr == stringify("1999-12"), + YearMonth.of(1999, 1).toSExpr == stringify("1999-01") + ) + }, + test("ZonedDateTime") { + val n = ZonedDateTime.now() + val ld = LocalDateTime.of(2020, 1, 1, 12, 36, 0) + val est = ZonedDateTime.of(ld, ZoneId.of("America/New_York")) + val utc = ZonedDateTime.of(ld, ZoneId.of("Etc/UTC")) + assertTrue( + n.toSExpr == stringify(n.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)), + est.toSExpr == stringify("2020-01-01T12:36:00-05:00[America/New_York]"), + utc.toSExpr == stringify("2020-01-01T12:36:00Z[Etc/UTC]") + ) + }, + test("ZoneId") { + assertTrue( + ZoneId.of("America/New_York").toSExpr == stringify("America/New_York"), + ZoneId.of("Etc/UTC").toSExpr == stringify("Etc/UTC"), + ZoneId.of("Pacific/Auckland").toSExpr == stringify("Pacific/Auckland"), + ZoneId.of("Asia/Shanghai").toSExpr == stringify("Asia/Shanghai"), + ZoneId.of("Africa/Cairo").toSExpr == stringify("Africa/Cairo") + ) + }, + test("ZoneOffset") { + assertTrue( + ZoneOffset.UTC.toSExpr == stringify("Z"), + ZoneOffset.ofHours(5).toSExpr == stringify("+05:00"), + ZoneOffset.ofHours(-5).toSExpr == stringify("-05:00") + ) + } + ), + suite("Decoder")( + test("DayOfWeek") { + assert(stringify("MONDAY").fromSExpr[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && + assert(stringify("TUESDAY").fromSExpr[DayOfWeek])(isRight(equalTo(DayOfWeek.TUESDAY))) && + assert(stringify("WEDNESDAY").fromSExpr[DayOfWeek])(isRight(equalTo(DayOfWeek.WEDNESDAY))) && + assert(stringify("THURSDAY").fromSExpr[DayOfWeek])(isRight(equalTo(DayOfWeek.THURSDAY))) && + assert(stringify("FRIDAY").fromSExpr[DayOfWeek])(isRight(equalTo(DayOfWeek.FRIDAY))) && + assert(stringify("SATURDAY").fromSExpr[DayOfWeek])(isRight(equalTo(DayOfWeek.SATURDAY))) && + assert(stringify("SUNDAY").fromSExpr[DayOfWeek])(isRight(equalTo(DayOfWeek.SUNDAY))) && + assert(stringify("monday").fromSExpr[DayOfWeek])( + isRight(equalTo(DayOfWeek.MONDAY)) + ) && + assert(stringify("MonDay").fromSExpr[DayOfWeek])( + isRight(equalTo(DayOfWeek.MONDAY)) + ) + }, + test("Duration") { + assert(stringify("PT24H").fromSExpr[Duration])(isRight(equalTo(Duration.ofHours(24)))) && + assert(stringify("-PT24H").fromSExpr[Duration])(isRight(equalTo(Duration.ofHours(-24)))) && + assert(stringify("P1D").fromSExpr[Duration])(isRight(equalTo(Duration.ofHours(24)))) && + assert(stringify("P1DT0H").fromSExpr[Duration])(isRight(equalTo(Duration.ofHours(24)))) && + assert(stringify("PT2562047788015215H30M7.999999999S").fromSExpr[Duration])( + isRight(equalTo(Duration.ofSeconds(Long.MaxValue, 999999999L))) + ) + }, + test("Instant") { + val n = Instant.now() + assert(stringify("1970-01-01T00:00:00Z").fromSExpr[Instant])(isRight(equalTo(Instant.EPOCH))) && + assert(stringify("1970-01-01T00:00:00.Z").fromSExpr[Instant])(isRight(equalTo(Instant.EPOCH))) && + assert(stringify(n).fromSExpr[Instant])(isRight(equalTo(n))) + }, + test("LocalDate") { + val n = LocalDate.now() + val p = LocalDate.of(2000, 2, 29) + assert(stringify(n).fromSExpr[LocalDate])(isRight(equalTo(n))) && + assert(stringify(p).fromSExpr[LocalDate])(isRight(equalTo(p))) + }, + test("LocalDateTime") { + val n = LocalDateTime.now() + val p = LocalDateTime.of(2020, 1, 1, 12, 36, 0) + assert(stringify(n).fromSExpr[LocalDateTime])(isRight(equalTo(n))) && + assert(stringify("2020-01-01T12:36").fromSExpr[LocalDateTime])(isRight(equalTo(p))) + assert(stringify("2020-01-01T12:36:00.").fromSExpr[LocalDateTime])(isRight(equalTo(p))) + }, + test("LocalTime") { + val n = LocalTime.now() + val p = LocalTime.of(12, 36, 0) + assert(stringify(n).fromSExpr[LocalTime])(isRight(equalTo(n))) && + assert(stringify("12:36").fromSExpr[LocalTime])(isRight(equalTo(p))) + assert(stringify("12:36:00.").fromSExpr[LocalTime])(isRight(equalTo(p))) + }, + test("Month") { + assert(stringify("JANUARY").fromSExpr[Month])(isRight(equalTo(Month.JANUARY))) && + assert(stringify("FEBRUARY").fromSExpr[Month])(isRight(equalTo(Month.FEBRUARY))) && + assert(stringify("MARCH").fromSExpr[Month])(isRight(equalTo(Month.MARCH))) && + assert(stringify("APRIL").fromSExpr[Month])(isRight(equalTo(Month.APRIL))) && + assert(stringify("MAY").fromSExpr[Month])(isRight(equalTo(Month.MAY))) && + assert(stringify("JUNE").fromSExpr[Month])(isRight(equalTo(Month.JUNE))) && + assert(stringify("JULY").fromSExpr[Month])(isRight(equalTo(Month.JULY))) && + assert(stringify("AUGUST").fromSExpr[Month])(isRight(equalTo(Month.AUGUST))) && + assert(stringify("SEPTEMBER").fromSExpr[Month])(isRight(equalTo(Month.SEPTEMBER))) && + assert(stringify("OCTOBER").fromSExpr[Month])(isRight(equalTo(Month.OCTOBER))) && + assert(stringify("NOVEMBER").fromSExpr[Month])(isRight(equalTo(Month.NOVEMBER))) && + assert(stringify("DECEMBER").fromSExpr[Month])(isRight(equalTo(Month.DECEMBER))) && + assert(stringify("december").fromSExpr[Month])(isRight(equalTo(Month.DECEMBER))) && + assert(stringify("December").fromSExpr[Month])(isRight(equalTo(Month.DECEMBER))) + }, + test("MonthDay") { + val n = MonthDay.now() + val p = MonthDay.of(1, 1) + assert(stringify(n).fromSExpr[MonthDay])(isRight(equalTo(n))) && + assert(stringify("--01-01").fromSExpr[MonthDay])(isRight(equalTo(p))) + }, + test("OffsetDateTime") { + val n = OffsetDateTime.now() + val p = OffsetDateTime.of(2020, 1, 1, 12, 36, 12, 0, ZoneOffset.UTC) + assert(stringify(n).fromSExpr[OffsetDateTime])(isRight(equalTo(n))) && + assert(stringify("2020-01-01T12:36:12Z").fromSExpr[OffsetDateTime])(isRight(equalTo(p))) + assert(stringify("2020-01-01T12:36:12.Z").fromSExpr[OffsetDateTime])(isRight(equalTo(p))) + }, + test("OffsetTime") { + val n = OffsetTime.now() + val p = OffsetTime.of(12, 36, 12, 0, ZoneOffset.ofHours(-4)) + assert(stringify(n).fromSExpr[OffsetTime])(isRight(equalTo(n))) && + assert(stringify("12:36:12-04:00").fromSExpr[OffsetTime])(isRight(equalTo(p))) + assert(stringify("12:36:12.-04:00").fromSExpr[OffsetTime])(isRight(equalTo(p))) + }, + test("Period") { + assert(stringify("P0D").fromSExpr[Period])(isRight(equalTo(Period.ZERO))) && + assert(stringify("P1D").fromSExpr[Period])(isRight(equalTo(Period.ofDays(1)))) && + assert(stringify("P-1D").fromSExpr[Period])(isRight(equalTo(Period.ofDays(-1)))) && + assert(stringify("-P1D").fromSExpr[Period])(isRight(equalTo(Period.ofDays(-1)))) && + assert(stringify("P2M").fromSExpr[Period])(isRight(equalTo(Period.ofMonths(2)))) && + assert(stringify("P364D").fromSExpr[Period])(isRight(equalTo(Period.ofWeeks(52)))) && + assert(stringify("P10Y").fromSExpr[Period])(isRight(equalTo(Period.ofYears(10)))) + }, + test("Year") { + val n = Year.now() + assert(stringify(n).fromSExpr[Year])(isRight(equalTo(n))) && + assert(stringify("1999").fromSExpr[Year])(isRight(equalTo(Year.of(1999)))) && + assert(stringify("+10000").fromSExpr[Year])(isRight(equalTo(Year.of(10000)))) + }, + test("YearMonth") { + val n = YearMonth.now() + assert(stringify(n).fromSExpr[YearMonth])(isRight(equalTo(n))) && + assert(stringify("1999-12").fromSExpr[YearMonth])(isRight(equalTo(YearMonth.of(1999, 12)))) && + assert(stringify("1999-01").fromSExpr[YearMonth])(isRight(equalTo(YearMonth.of(1999, 1)))) + }, + test("ZonedDateTime") { + def zdtAssert(actual: String, expected: ZonedDateTime): TestResult = + assert(stringify(actual).fromSExpr[ZonedDateTime].map(_.toString))(isRight(equalTo(expected.toString))) + + val n = ZonedDateTime.now() + val ld = LocalDateTime.of(2020, 1, 1, 12, 36, 0) + val est = ZonedDateTime.of(ld, ZoneId.of("America/New_York")) + val utc = ZonedDateTime.of(ld, ZoneId.of("Etc/UTC")) + val gmt = ZonedDateTime.of(ld, ZoneId.of("+00:00")) + + zdtAssert(n.toString, n) && + zdtAssert("2020-01-01T12:36:00-05:00[America/New_York]", est) && + zdtAssert("2020-01-01T12:36:00Z[Etc/UTC]", utc) && + zdtAssert("2020-01-01T12:36:00.Z[Etc/UTC]", utc) && + zdtAssert("2020-01-01T12:36:00+00:00[+00:00]", gmt) && + zdtAssert( + "2018-02-01T00:00Z", + ZonedDateTime.of(LocalDateTime.of(2018, 2, 1, 0, 0, 0), ZoneOffset.UTC) + ) && + zdtAssert( + "2018-03-01T00:00:00Z", + ZonedDateTime.of(LocalDateTime.of(2018, 3, 1, 0, 0, 0), ZoneOffset.UTC) + ) && + zdtAssert( + "2018-04-01T00:00:00.000Z", + ZonedDateTime.of(LocalDateTime.of(2018, 4, 1, 0, 0, 0), ZoneOffset.UTC) + ) && + zdtAssert( + "+999999999-12-31T23:59:59.999999999+18:00", + ZonedDateTime.of(LocalDateTime.MAX, ZoneOffset.MAX) + ) && + zdtAssert( + "+999999999-12-31T23:59:59.999999999-18:00", + ZonedDateTime.of(LocalDateTime.MAX, ZoneOffset.MIN) + ) && + zdtAssert("-999999999-01-01T00:00:00+18:00", ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MAX)) && + zdtAssert("-999999999-01-01T00:00:00-18:00", ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MIN)) && + zdtAssert( + "2012-10-28T02:00:00+01:00[Europe/Berlin]", + OffsetDateTime.parse("2012-10-28T02:00:00+01:00").atZoneSameInstant(ZoneId.of("Europe/Berlin")) + ) && + zdtAssert( + "2018-03-25T02:30+01:00[Europe/Warsaw]", + ZonedDateTime.parse("2018-03-25T02:30+01:00[Europe/Warsaw]") + ) && + zdtAssert( + "2018-03-25T02:30+00:00[Europe/Warsaw]", + OffsetDateTime.parse("2018-03-25T02:30+00:00").atZoneSameInstant(ZoneId.of("Europe/Warsaw")) + ) && + zdtAssert( + "2018-03-25T02:30+02:00[Europe/Warsaw]", + OffsetDateTime.parse("2018-03-25T02:30+02:00").atZoneSameInstant(ZoneId.of("Europe/Warsaw")) + ) && + zdtAssert( + "2018-03-25T02:30+03:00[Europe/Warsaw]", + OffsetDateTime.parse("2018-03-25T02:30+03:00").atZoneSameInstant(ZoneId.of("Europe/Warsaw")) + ) && + zdtAssert( + "2018-10-28T02:30+00:00[Europe/Warsaw]", + OffsetDateTime.parse("2018-10-28T02:30+00:00").atZoneSameInstant(ZoneId.of("Europe/Warsaw")) + ) && + zdtAssert( + "2018-10-28T02:30+01:00[Europe/Warsaw]", + OffsetDateTime.parse("2018-10-28T02:30+01:00").atZoneSameInstant(ZoneId.of("Europe/Warsaw")) + ) && + zdtAssert( + "2018-10-28T02:30+02:00[Europe/Warsaw]", + OffsetDateTime.parse("2018-10-28T02:30+02:00").atZoneSameInstant(ZoneId.of("Europe/Warsaw")) + ) && + zdtAssert( + "2018-10-28T02:30+03:00[Europe/Warsaw]", + OffsetDateTime.parse("2018-10-28T02:30+03:00").atZoneSameInstant(ZoneId.of("Europe/Warsaw")) + ) + }, + test("ZoneId") { + assert(stringify("America/New_York").fromSExpr[ZoneId])( + isRight( + equalTo( + ZoneId.of("America/New_York") + ) + ) + ) && + assert(stringify("Etc/UTC").fromSExpr[ZoneId])(isRight(equalTo(ZoneId.of("Etc/UTC")))) && + assert(stringify("Pacific/Auckland").fromSExpr[ZoneId])( + isRight( + equalTo( + ZoneId.of("Pacific/Auckland") + ) + ) + ) && + assert(stringify("Asia/Shanghai").fromSExpr[ZoneId])( + isRight(equalTo(ZoneId.of("Asia/Shanghai"))) + ) && + assert(stringify("Africa/Cairo").fromSExpr[ZoneId])(isRight(equalTo(ZoneId.of("Africa/Cairo")))) + }, + test("ZoneOffset") { + assert(stringify("Z").fromSExpr[ZoneOffset])(isRight(equalTo(ZoneOffset.UTC))) && + assert(stringify("+05:00").fromSExpr[ZoneOffset])(isRight(equalTo(ZoneOffset.ofHours(5)))) && + assert(stringify("-05:00").fromSExpr[ZoneOffset])(isRight(equalTo(ZoneOffset.ofHours(-5)))) + } + ), + suite("Decoder Sad Path")( + test("DayOfWeek") { + assert(stringify("foody").fromSExpr[DayOfWeek])( + isLeft( + equalTo("(No enum constant java.time.DayOfWeek.FOODY)") || // JVM + equalTo("(Unrecognized day of week name: FOODY)") || + equalTo("(enum case not found: FOODY)") + ) // Scala.js + ) + }, + test("Duration") { + assert("""""""".fromSExpr[Duration])( + isLeft(containsString(" is not a valid ISO-8601 format, illegal duration at index 0")) + ) && + assert(""""X"""".fromSExpr[Duration])( + isLeft(containsString("X is not a valid ISO-8601 format, expected 'P' or '-' at index 0")) + ) && + assert(""""P"""".fromSExpr[Duration])( + isLeft(containsString("P is not a valid ISO-8601 format, illegal duration at index 1")) + ) && + assert(""""-"""".fromSExpr[Duration])( + isLeft(containsString("- is not a valid ISO-8601 format, illegal duration at index 1")) + ) && + assert(""""-X"""".fromSExpr[Duration])( + isLeft(containsString("-X is not a valid ISO-8601 format, expected 'P' at index 1")) + ) && + assert(""""PXD"""".fromSExpr[Duration])( + isLeft(containsString("PXD is not a valid ISO-8601 format, expected '-' or digit at index 1")) + ) && + assert(""""P-"""".fromSExpr[Duration])( + isLeft(containsString("P- is not a valid ISO-8601 format, illegal duration at index 2")) + ) && + assert(""""P-XD"""".fromSExpr[Duration])( + isLeft(containsString("P-XD is not a valid ISO-8601 format, expected digit at index 2")) + ) && + assert(""""P1XD"""".fromSExpr[Duration])( + isLeft(containsString("P1XD is not a valid ISO-8601 format, expected 'D' or digit at index 2")) + ) && + assert(""""PT"""".fromSExpr[Duration])( + isLeft(containsString("PT is not a valid ISO-8601 format, illegal duration at index 2")) + ) && + assert(""""PT0SX"""".fromSExpr[Duration])( + isLeft(containsString("PT0SX is not a valid ISO-8601 format, illegal duration at index 4")) + ) && + assert(""""P1DT"""".fromSExpr[Duration])( + isLeft(containsString("P1DT is not a valid ISO-8601 format, illegal duration at index 4")) + ) && + assert(""""P106751991167301D"""".fromSExpr[Duration])( + isLeft(containsString("P106751991167301D is not a valid ISO-8601 format, illegal duration at index 16")) + ) && + assert(""""P1067519911673000D"""".fromSExpr[Duration])( + isLeft(containsString("P1067519911673000D is not a valid ISO-8601 format, illegal duration at index 17")) + ) && + assert(""""P-106751991167301D"""".fromSExpr[Duration])( + isLeft(containsString("P-106751991167301D is not a valid ISO-8601 format, illegal duration at index 17")) + ) && + assert(""""P1DX1H"""".fromSExpr[Duration])( + isLeft(containsString("P1DX1H is not a valid ISO-8601 format, expected 'T' or '\"' at index 3")) + ) && + assert(""""P1DTXH"""".fromSExpr[Duration])( + isLeft(containsString("P1DTXH is not a valid ISO-8601 format, expected '-' or digit at index 4")) + ) && + assert(""""P1DT-XH"""".fromSExpr[Duration])( + isLeft(containsString("P1DT-XH is not a valid ISO-8601 format, expected digit at index 5")) + ) && + assert(""""P1DT1XH"""".fromSExpr[Duration])( + isLeft( + containsString( + "P1DT1XH is not a valid ISO-8601 format, expected 'H' or 'M' or 'S or '.' or digit at index 5" + ) + ) + ) && + assert(""""P1DT1H1XM"""".fromSExpr[Duration])( + isLeft( + containsString("P1DT1H1XM is not a valid ISO-8601 format, expected 'M' or 'S or '.' or digit at index 7") + ) + ) && + assert(""""P0DT2562047788015216H"""".fromSExpr[Duration])( + isLeft(containsString("P0DT2562047788015216H is not a valid ISO-8601 format, illegal duration at index 20")) + ) && + assert(""""P0DT-2562047788015216H"""".fromSExpr[Duration])( + isLeft( + containsString("P0DT-2562047788015216H is not a valid ISO-8601 format, illegal duration at index 21") + ) + ) && + assert(""""P0DT153722867280912931M"""".fromSExpr[Duration])( + isLeft( + containsString("P0DT153722867280912931M is not a valid ISO-8601 format, illegal duration at index 22") + ) + ) && + assert(""""P0DT-153722867280912931M"""".fromSExpr[Duration])( + isLeft( + containsString("P0DT-153722867280912931M is not a valid ISO-8601 format, illegal duration at index 23") + ) + ) && + assert(""""P0DT9223372036854775808S"""".fromSExpr[Duration])( + isLeft( + containsString("P0DT9223372036854775808S is not a valid ISO-8601 format, illegal duration at index 23") + ) + ) && + assert(""""P0DT92233720368547758000S"""".fromSExpr[Duration])( + isLeft( + containsString("P0DT92233720368547758000S is not a valid ISO-8601 format, illegal duration at index 23") + ) + ) && + assert(""""P0DT-9223372036854775809S"""".fromSExpr[Duration])( + isLeft( + containsString("P0DT-9223372036854775809S is not a valid ISO-8601 format, illegal duration at index 23") + ) + ) && + assert(""""P1DT1H1MXS"""".fromSExpr[Duration])( + isLeft( + containsString("P1DT1H1MXS is not a valid ISO-8601 format, expected '\"' or '-' or digit at index 8") + ) + ) && + assert(""""P1DT1H1M-XS"""".fromSExpr[Duration])( + isLeft(containsString("P1DT1H1M-XS is not a valid ISO-8601 format, expected digit at index 9")) + ) && + assert(""""P1DT1H1M0XS"""".fromSExpr[Duration])( + isLeft(containsString("P1DT1H1M0XS is not a valid ISO-8601 format, expected 'S or '.' or digit at index 9")) + ) && + assert(""""P1DT1H1M0.XS"""".fromSExpr[Duration])( + isLeft(containsString("P1DT1H1M0.XS is not a valid ISO-8601 format, expected 'S' or digit at index 10")) + ) && + assert(""""P1DT1H1M0.012345678XS"""".fromSExpr[Duration])( + isLeft(containsString("P1DT1H1M0.012345678XS is not a valid ISO-8601 format, expected 'S' at index 19")) + ) && + assert(""""P1DT1H1M0.0123456789S"""".fromSExpr[Duration])( + isLeft(containsString("P1DT1H1M0.0123456789S is not a valid ISO-8601 format, expected 'S' at index 19")) + ) && + assert(""""P0DT0H0M9223372036854775808S"""".fromSExpr[Duration])( + isLeft( + containsString( + "P0DT0H0M9223372036854775808S is not a valid ISO-8601 format, illegal duration at index 27" + ) + ) + ) && + assert(""""P0DT0H0M92233720368547758080S"""".fromSExpr[Duration])( + isLeft( + containsString( + "P0DT0H0M92233720368547758080S is not a valid ISO-8601 format, illegal duration at index 27" + ) + ) + ) && + assert(""""P0DT0H0M-9223372036854775809S"""".fromSExpr[Duration])( + isLeft( + containsString( + "P0DT0H0M-9223372036854775809S is not a valid ISO-8601 format, illegal duration at index 27" + ) + ) + ) && + assert(""""P106751991167300DT24H"""".fromSExpr[Duration])( + isLeft(containsString("P106751991167300DT24H is not a valid ISO-8601 format, illegal duration at index 20")) + ) && + assert(""""P0DT2562047788015215H60M"""".fromSExpr[Duration])( + isLeft( + containsString("P0DT2562047788015215H60M is not a valid ISO-8601 format, illegal duration at index 23") + ) + ) && + assert(""""P0DT0H153722867280912930M60S"""".fromSExpr[Duration])( + isLeft( + containsString( + "P0DT0H153722867280912930M60S is not a valid ISO-8601 format, illegal duration at index 27" + ) + ) + ) + }, + test("Instant") { + assert(stringify("").fromSExpr[Instant])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal instant at index 0)") + ) + ) && + assert(stringify("2020").fromSExpr[Instant])( + isLeft( + equalTo("(2020 is not a valid ISO-8601 format, illegal instant at index 0)") + ) + ) && + assert(stringify("2020-0").fromSExpr[Instant])( + isLeft( + equalTo("(2020-0 is not a valid ISO-8601 format, illegal instant at index 5)") + ) + ) && + assert(stringify("2020-01-0").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal instant at index 8)") + ) + ) && + assert(stringify("2020-01-01T0").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal instant at index 11)") + ) + ) && + assert(stringify("2020-01-01T01:0").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal instant at index 14)") + ) + ) && + assert(stringify("X020-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(X020-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") + ) + ) && + assert(stringify("2X20-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2X20-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("20X0-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(20X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("202X-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(202X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("2020X01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020X01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 4)") + ) + ) && + assert(stringify("2020-X1-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-X1-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("2020-0X-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-0X-01T01:01Z is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("2020-01X01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01X01T01:01Z is not a valid ISO-8601 format, expected '-' at index 7)") + ) + ) && + assert(stringify("2020-01-X1T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-X1T01:01Z is not a valid ISO-8601 format, expected digit at index 8)") + ) + ) && + assert(stringify("2020-01-0XT01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-0XT01:01Z is not a valid ISO-8601 format, expected digit at index 9)") + ) + ) && + assert(stringify("2020-01-01X01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01X01:01Z is not a valid ISO-8601 format, expected 'T' at index 10)") + ) + ) && + assert(stringify("2020-01-01TX1:01").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") + ) + ) && + assert(stringify("2020-01-01T0X:01").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") + ) + ) && + assert(stringify("2020-01-01T24:01").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") + ) + ) && + assert(stringify("2020-01-01T01X01").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") + ) + ) && + assert(stringify("2020-01-01T01:X1").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") + ) + ) && + assert(stringify("2020-01-01T01:0X").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:60").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:01X").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' or 'Z' at index 16)") + ) + ) && + assert(stringify("2020-01-01T01:01:0").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal instant at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:X1Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:X1Z is not a valid ISO-8601 format, expected digit at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:0XZ").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:0XZ is not a valid ISO-8601 format, expected digit at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:60Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:60Z is not a valid ISO-8601 format, illegal second at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:012").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' or 'Z' at index 19)") + ) + ) && + assert(stringify("2020-01-01T01:01:01.X").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:01.X is not a valid ISO-8601 format, expected digit or 'Z' at index 20)") + ) + ) && + assert(stringify("2020-01-01T01:01:01.123456789X").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:01.123456789X is not a valid ISO-8601 format, expected 'Z' at index 29)") + ) + ) && + assert(stringify("2020-01-01T01:01:01ZX").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-01T01:01:01ZX is not a valid ISO-8601 format, illegal instant at index 20)") + ) + ) && + assert(stringify("+X0000-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+X0000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+1X000-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+1X000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+10X00-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+10X00-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+100X0-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+100X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+1000X-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+1000X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+10000X-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+10000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 6)") + ) + ) && + assert(stringify("+100000X-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+100000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 7)") + ) + ) && + assert(stringify("+1000000X-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+1000000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 8)") + ) + ) && + assert(stringify("+1000000001-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+1000000001-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 10)") + ) + ) && + assert(stringify("+3333333333-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(+3333333333-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 10)") + ) + ) && + assert(stringify("-1000000001-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(-1000000001-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 10)") + ) + ) && + assert(stringify("-0000-01-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(-0000-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) && + assert(stringify("+10000").fromSExpr[Instant])( + isLeft( + equalTo("(+10000 is not a valid ISO-8601 format, illegal instant at index 6)") + ) + ) && + assert(stringify("2020-00-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-00-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-13-01T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-13-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-01-00T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-00T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-01-32T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-01-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-02-30T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-02-30T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-03-32T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-03-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-04-31T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-04-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-05-32T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-05-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-06-31T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-06-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-07-32T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-07-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-08-32T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-08-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-09-31T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-09-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-10-32T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-10-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-11-31T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-11-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-12-32T01:01Z").fromSExpr[Instant])( + isLeft( + equalTo("(2020-12-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) + }, + test("LocalDate") { + assert(stringify("").fromSExpr[LocalDate])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal local date at index 0)") + ) + ) && + assert(stringify("2020").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020 is not a valid ISO-8601 format, illegal local date at index 0)") + ) + ) && + assert(stringify("2020-0").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-0 is not a valid ISO-8601 format, illegal local date at index 5)") + ) + ) && + assert(stringify("2020-01-0").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal local date at index 8)") + ) + ) && + assert(stringify("2020-01-012").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-01-012 is not a valid ISO-8601 format, illegal local date at index 10)") + ) + ) && + assert(stringify("X020-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(X020-01-01 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") + ) + ) && + assert(stringify("2X20-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(2X20-01-01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("20X0-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(20X0-01-01 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("202X-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(202X-01-01 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("2020X01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020X01-01 is not a valid ISO-8601 format, expected '-' at index 4)") + ) + ) && + assert(stringify("2020-X1-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-X1-01 is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("2020-0X-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-0X-01 is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("2020-01X01").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-01X01 is not a valid ISO-8601 format, expected '-' at index 7)") + ) + ) && + assert(stringify("2020-01-X1").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-01-X1 is not a valid ISO-8601 format, expected digit at index 8)") + ) + ) && + assert(stringify("2020-01-0X").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-01-0X is not a valid ISO-8601 format, expected digit at index 9)") + ) + ) && + assert(stringify("+X0000-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+X0000-01-01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+1X000-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+1X000-01-01 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+10X00-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+10X00-01-01 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+100X0-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+100X0-01-01 is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+1000X-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+1000X-01-01 is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+10000X-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+10000X-01-01 is not a valid ISO-8601 format, expected '-' or digit at index 6)") + ) + ) && + assert(stringify("+100000X-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+100000X-01-01 is not a valid ISO-8601 format, expected '-' or digit at index 7)") + ) + ) && + assert(stringify("+1000000X-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+1000000X-01-01 is not a valid ISO-8601 format, expected '-' or digit at index 8)") + ) + ) && + assert(stringify("+1000000000-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(+1000000000-01-01 is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-1000000000-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(-1000000000-01-01 is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-0000-01-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(-0000-01-01 is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) && + assert(stringify("+10000").fromSExpr[LocalDate])( + isLeft( + equalTo("(+10000 is not a valid ISO-8601 format, illegal local date at index 6)") + ) + ) && + assert(stringify("2020-00-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-00-01 is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-13-01").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-13-01 is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-01-00").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-01-00 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-01-32").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-01-32 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-02-30").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-02-30 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-03-32").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-03-32 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-04-31").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-04-31 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-05-32").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-05-32 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-06-31").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-06-31 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-07-32").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-07-32 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-08-32").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-08-32 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-09-31").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-09-31 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-10-32").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-10-32 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-11-31").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-11-31 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-12-32").fromSExpr[LocalDate])( + isLeft( + equalTo("(2020-12-32 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) + }, + test("LocalDateTime") { + assert(stringify("").fromSExpr[LocalDateTime])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal local date time at index 0)") + ) + ) && + assert(stringify("2020").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020 is not a valid ISO-8601 format, illegal local date time at index 0)") + ) + ) && + assert(stringify("2020-0").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-0 is not a valid ISO-8601 format, illegal local date time at index 5)") + ) + ) && + assert(stringify("2020-01-0").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal local date time at index 8)") + ) + ) && + assert(stringify("2020-01-01T0").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal local date time at index 11)") + ) + ) && + assert(stringify("2020-01-01T01:0").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal local date time at index 14)") + ) + ) && + assert(stringify("X020-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(X020-01-01T01:01 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") + ) + ) && + assert(stringify("2X20-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2X20-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("20X0-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(20X0-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("202X-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(202X-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("2020X01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020X01-01T01:01 is not a valid ISO-8601 format, expected '-' at index 4)") + ) + ) && + assert(stringify("2020-X1-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-X1-01T01:01 is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("2020-0X-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-0X-01T01:01 is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("2020-01X01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01X01T01:01 is not a valid ISO-8601 format, expected '-' at index 7)") + ) + ) && + assert(stringify("2020-01-X1T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-X1T01:01 is not a valid ISO-8601 format, expected digit at index 8)") + ) + ) && + assert(stringify("2020-01-0XT01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-0XT01:01 is not a valid ISO-8601 format, expected digit at index 9)") + ) + ) && + assert(stringify("2020-01-01X01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01X01:01 is not a valid ISO-8601 format, expected 'T' at index 10)") + ) + ) && + assert(stringify("2020-01-01TX1:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") + ) + ) && + assert(stringify("2020-01-01T0X:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") + ) + ) && + assert(stringify("2020-01-01T24:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") + ) + ) && + assert(stringify("2020-01-01T01X01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") + ) + ) && + assert(stringify("2020-01-01T01:X1").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") + ) + ) && + assert(stringify("2020-01-01T01:0X").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:60").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:01X").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' at index 16)") + ) + ) && + assert(stringify("2020-01-01T01:01:0").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal local date time at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:X1").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:X1 is not a valid ISO-8601 format, expected digit at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:0X").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:0X is not a valid ISO-8601 format, expected digit at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:60").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:60 is not a valid ISO-8601 format, illegal second at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:012").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' at index 19)") + ) + ) && + assert(stringify("2020-01-01T01:01:01.X").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01.X is not a valid ISO-8601 format, illegal local date time at index 20)") + ) + ) && + assert(stringify("+X0000-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+X0000-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+1X000-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+1X000-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+10X00-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+10X00-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+100X0-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+100X0-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+1000X-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+1000X-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+10000X-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+10000X-01-01T01:01 is not a valid ISO-8601 format, expected '-' or digit at index 6)") + ) + ) && + assert(stringify("+100000X-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+100000X-01-01T01:01 is not a valid ISO-8601 format, expected '-' or digit at index 7)") + ) + ) && + assert(stringify("+1000000X-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+1000000X-01-01T01:01 is not a valid ISO-8601 format, expected '-' or digit at index 8)") + ) + ) && + assert(stringify("+1000000000-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+1000000000-01-01T01:01 is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-1000000000-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(-1000000000-01-01T01:01 is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-0000-01-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(-0000-01-01T01:01 is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) && + assert(stringify("+10000").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(+10000 is not a valid ISO-8601 format, illegal local date time at index 6)") + ) + ) && + assert(stringify("2020-00-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-00-01T01:01 is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-13-01T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-13-01T01:01 is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-01-00T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-00T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-01-32T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-01-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-02-30T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-02-30T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-03-32T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-03-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-04-31T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-04-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-05-32T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-05-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-06-31T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-06-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-07-32T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-07-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-08-32T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-08-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-09-31T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-09-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-10-32T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-10-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-11-31T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-11-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-12-32T01:01").fromSExpr[LocalDateTime])( + isLeft( + equalTo("(2020-12-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) + }, + test("LocalTime") { + assert(stringify("").fromSExpr[LocalTime])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal local time at index 0)") + ) + ) && + assert(stringify("0").fromSExpr[LocalTime])( + isLeft( + equalTo("(0 is not a valid ISO-8601 format, illegal local time at index 0)") + ) + ) && + assert(stringify("01:0").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:0 is not a valid ISO-8601 format, illegal local time at index 3)") + ) + ) && + assert(stringify("X1:01").fromSExpr[LocalTime])( + isLeft( + equalTo("(X1:01 is not a valid ISO-8601 format, expected digit at index 0)") + ) + ) && + assert(stringify("0X:01").fromSExpr[LocalTime])( + isLeft( + equalTo("(0X:01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("24:01").fromSExpr[LocalTime])( + isLeft( + equalTo("(24:01 is not a valid ISO-8601 format, illegal hour at index 1)") + ) + ) && + assert(stringify("01X01").fromSExpr[LocalTime])( + isLeft( + equalTo("(01X01 is not a valid ISO-8601 format, expected ':' at index 2)") + ) + ) && + assert(stringify("01:X1").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:X1 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("01:0X").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:0X is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("01:60").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:60 is not a valid ISO-8601 format, illegal minute at index 4)") + ) + ) && + assert(stringify("01:01X").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:01X is not a valid ISO-8601 format, expected ':' at index 5)") + ) + ) && + assert(stringify("01:01:0").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:01:0 is not a valid ISO-8601 format, illegal local time at index 6)") + ) + ) && + assert(stringify("01:01:X1").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:01:X1 is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("01:01:0X").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:01:0X is not a valid ISO-8601 format, expected digit at index 7)") + ) + ) && + assert(stringify("01:01:60").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:01:60 is not a valid ISO-8601 format, illegal second at index 7)") + ) + ) && + assert(stringify("01:01:012").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:01:012 is not a valid ISO-8601 format, expected '.' at index 8)") + ) + ) && + assert(stringify("01:01:01.X").fromSExpr[LocalTime])( + isLeft( + equalTo("(01:01:01.X is not a valid ISO-8601 format, illegal local time at index 9)") + ) + ) + }, + test("Month") { + assert(stringify("FebTober").fromSExpr[Month])( + isLeft( + equalTo("(No enum constant java.time.Month.FEBTOBER)") || // JVM + equalTo("(Unrecognized month name: FEBTOBER)") || + equalTo("(enum case not found: FEBTOBER)") + ) // Scala.js + ) + }, + test("MonthDay") { + assert(stringify("").fromSExpr[MonthDay])( + isLeft(equalTo("( is not a valid ISO-8601 format, illegal month day at index 0)")) + ) && + assert(stringify("X-01-01").fromSExpr[MonthDay])( + isLeft(equalTo("(X-01-01 is not a valid ISO-8601 format, expected '-' at index 0)")) + ) && + assert(stringify("-X01-01").fromSExpr[MonthDay])( + isLeft(equalTo("(-X01-01 is not a valid ISO-8601 format, expected '-' at index 1)")) + ) && + assert(stringify("--X1-01").fromSExpr[MonthDay])( + isLeft(equalTo("(--X1-01 is not a valid ISO-8601 format, expected digit at index 2)")) + ) && + assert(stringify("--0X-01").fromSExpr[MonthDay])( + isLeft(equalTo("(--0X-01 is not a valid ISO-8601 format, expected digit at index 3)")) + ) && + assert(stringify("--00-01").fromSExpr[MonthDay])( + isLeft(equalTo("(--00-01 is not a valid ISO-8601 format, illegal month at index 3)")) + ) && + assert(stringify("--13-01").fromSExpr[MonthDay])( + isLeft(equalTo("(--13-01 is not a valid ISO-8601 format, illegal month at index 3)")) + ) && + assert(stringify("--01X01").fromSExpr[MonthDay])( + isLeft(equalTo("(--01X01 is not a valid ISO-8601 format, expected '-' at index 4)")) + ) && + assert(stringify("--01-X1").fromSExpr[MonthDay])( + isLeft(equalTo("(--01-X1 is not a valid ISO-8601 format, expected digit at index 5)")) + ) && + assert(stringify("--01-0X").fromSExpr[MonthDay])( + isLeft(equalTo("(--01-0X is not a valid ISO-8601 format, expected digit at index 6)")) + ) && + assert(stringify("--01-00").fromSExpr[MonthDay])( + isLeft(equalTo("(--01-00 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--01-32").fromSExpr[MonthDay])( + isLeft(equalTo("(--01-32 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--02-30").fromSExpr[MonthDay])( + isLeft(equalTo("(--02-30 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--03-32").fromSExpr[MonthDay])( + isLeft(equalTo("(--03-32 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--04-31").fromSExpr[MonthDay])( + isLeft(equalTo("(--04-31 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--05-32").fromSExpr[MonthDay])( + isLeft(equalTo("(--05-32 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--06-31").fromSExpr[MonthDay])( + isLeft(equalTo("(--06-31 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--07-32").fromSExpr[MonthDay])( + isLeft(equalTo("(--07-32 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--08-32").fromSExpr[MonthDay])( + isLeft(equalTo("(--08-32 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--09-31").fromSExpr[MonthDay])( + isLeft(equalTo("(--09-31 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--10-32").fromSExpr[MonthDay])( + isLeft(equalTo("(--10-32 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--11-31").fromSExpr[MonthDay])( + isLeft(equalTo("(--11-31 is not a valid ISO-8601 format, illegal day at index 6)")) + ) && + assert(stringify("--12-32").fromSExpr[MonthDay])( + isLeft(equalTo("(--12-32 is not a valid ISO-8601 format, illegal day at index 6)")) + ) + }, + test("OffsetDateTime") { + assert(stringify("").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal offset date time at index 0)") + ) + ) && + assert(stringify("2020").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020 is not a valid ISO-8601 format, illegal offset date time at index 0)") + ) + ) && + assert(stringify("2020-0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-0 is not a valid ISO-8601 format, illegal offset date time at index 5)") + ) + ) && + assert(stringify("2020-01-0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal offset date time at index 8)") + ) + ) && + assert(stringify("2020-01-01T0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal offset date time at index 11)") + ) + ) && + assert(stringify("2020-01-01T01:0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal offset date time at index 14)") + ) + ) && + assert(stringify("X020-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(X020-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") + ) + ) && + assert(stringify("2X20-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2X20-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("20X0-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(20X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("202X-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(202X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("2020X01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020X01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 4)") + ) + ) && + assert(stringify("2020-X1-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-X1-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("2020-0X-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-0X-01T01:01Z is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("2020-01X01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01X01T01:01Z is not a valid ISO-8601 format, expected '-' at index 7)") + ) + ) && + assert(stringify("2020-01-X1T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-X1T01:01Z is not a valid ISO-8601 format, expected digit at index 8)") + ) + ) && + assert(stringify("2020-01-0XT01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-0XT01:01Z is not a valid ISO-8601 format, expected digit at index 9)") + ) + ) && + assert(stringify("2020-01-01X01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01X01:01Z is not a valid ISO-8601 format, expected 'T' at index 10)") + ) + ) && + assert(stringify("2020-01-01TX1:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") + ) + ) && + assert(stringify("2020-01-01T0X:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") + ) + ) && + assert(stringify("2020-01-01T24:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") + ) + ) && + assert(stringify("2020-01-01T01X01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") + ) + ) && + assert(stringify("2020-01-01T01:X1").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") + ) + ) && + assert(stringify("2020-01-01T01:0X").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:60").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01X").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal offset date time at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:X1Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:X1Z is not a valid ISO-8601 format, expected digit at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:0XZ").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:0XZ is not a valid ISO-8601 format, expected digit at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:60Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:60Z is not a valid ISO-8601 format, illegal second at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:012").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01.").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01. is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01.X").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01.X is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01.123456789X").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01.123456789X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 29)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01ZX").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01ZX is not a valid ISO-8601 format, illegal offset date time at index 20)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+X1:01:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+X1:01:01 is not a valid ISO-8601 format, expected digit at index 20)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+0 is not a valid ISO-8601 format, illegal offset date time at index 20)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+0X:01:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+0X:01:01 is not a valid ISO-8601 format, expected digit at index 21)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+19:01:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 21)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01X01:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01X01:01 is not a valid ISO-8601 format, illegal offset date time at index 23)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:0 is not a valid ISO-8601 format, illegal offset date time at index 23)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:X1:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:X1:01 is not a valid ISO-8601 format, expected digit at index 23)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:0X:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:0X:01 is not a valid ISO-8601 format, expected digit at index 24)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:60:01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 24)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01X01").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:01X01 is not a valid ISO-8601 format, illegal offset date time at index 26)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:0").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:01:0 is not a valid ISO-8601 format, illegal offset date time at index 26)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:X1").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:01:X1 is not a valid ISO-8601 format, expected digit at index 26)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:0X").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:01:0X is not a valid ISO-8601 format, expected digit at index 27)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:60").fromSExpr[OffsetDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 27)" + ) + ) + ) && + assert(stringify("+X0000-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+X0000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+1X000-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+1X000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+10X00-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+10X00-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+100X0-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+100X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+1000X-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+1000X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+10000X-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+10000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 6)") + ) + ) && + assert(stringify("+100000X-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+100000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 7)") + ) + ) && + assert(stringify("+1000000X-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+1000000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 8)") + ) + ) && + assert(stringify("+1000000000-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-1000000000-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(-1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-0000-01-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(-0000-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) && + assert(stringify("+10000").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(+10000 is not a valid ISO-8601 format, illegal offset date time at index 6)") + ) + ) && + assert(stringify("2020-00-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-00-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-13-01T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-13-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-01-00T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-00T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-01-32T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-01-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-02-30T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-02-30T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-03-32T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-03-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-04-31T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-04-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-05-32T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-05-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-06-31T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-06-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-07-32T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-07-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-08-32T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-08-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-09-31T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-09-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-10-32T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-10-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-11-31T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-11-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-12-32T01:01Z").fromSExpr[OffsetDateTime])( + isLeft( + equalTo("(2020-12-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) + }, + test("OffsetTime") { + assert(stringify("").fromSExpr[OffsetTime])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal offset time at index 0)") + ) + ) && + assert(stringify("0").fromSExpr[OffsetTime])( + isLeft( + equalTo("(0 is not a valid ISO-8601 format, illegal offset time at index 0)") + ) + ) && + assert(stringify("01:0").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:0 is not a valid ISO-8601 format, illegal offset time at index 3)") + ) + ) && + assert(stringify("X1:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(X1:01 is not a valid ISO-8601 format, expected digit at index 0)") + ) + ) && + assert(stringify("0X:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(0X:01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("24:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(24:01 is not a valid ISO-8601 format, illegal hour at index 1)") + ) + ) && + assert(stringify("01X01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01X01 is not a valid ISO-8601 format, expected ':' at index 2)") + ) + ) && + assert(stringify("01:X1").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:X1 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("01:0X").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:0X is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("01:60").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:60 is not a valid ISO-8601 format, illegal minute at index 4)") + ) + ) && + assert(stringify("01:01").fromSExpr[OffsetTime])( + isLeft( + equalTo( + "(01:01 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 5)" + ) + ) + ) && + assert(stringify("01:01X").fromSExpr[OffsetTime])( + isLeft( + equalTo( + "(01:01X is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 5)" + ) + ) + ) && + assert(stringify("01:01:0").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:0 is not a valid ISO-8601 format, illegal offset time at index 6)") + ) + ) && + assert(stringify("01:01:X1Z").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:X1Z is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("01:01:0XZ").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:0XZ is not a valid ISO-8601 format, expected digit at index 7)") + ) + ) && + assert(stringify("01:01:60Z").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:60Z is not a valid ISO-8601 format, illegal second at index 7)") + ) + ) && + assert(stringify("01:01:01").fromSExpr[OffsetTime])( + isLeft( + equalTo( + "(01:01:01 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 8)" + ) + ) + ) && + assert(stringify("01:01:012").fromSExpr[OffsetTime])( + isLeft( + equalTo( + "(01:01:012 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 8)" + ) + ) + ) && + assert(stringify("01:01:01.").fromSExpr[OffsetTime])( + isLeft( + equalTo( + "(01:01:01. is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 9)" + ) + ) + ) && + assert(stringify("01:01:01.X").fromSExpr[OffsetTime])( + isLeft( + equalTo( + "(01:01:01.X is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 9)" + ) + ) + ) && + assert(stringify("01:01:01.123456789X").fromSExpr[OffsetTime])( + isLeft( + equalTo( + "(01:01:01.123456789X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 18)" + ) + ) + ) && + assert(stringify("01:01:01ZX").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01ZX is not a valid ISO-8601 format, illegal offset time at index 9)") + ) + ) && + assert(stringify("01:01:01+X1:01:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+X1:01:01 is not a valid ISO-8601 format, expected digit at index 9)") + ) + ) && + assert(stringify("01:01:01+0").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+0 is not a valid ISO-8601 format, illegal offset time at index 9)") + ) + ) && + assert(stringify("01:01:01+0X:01:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+0X:01:01 is not a valid ISO-8601 format, expected digit at index 10)") + ) + ) && + assert(stringify("01:01:01+19:01:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 10)") + ) + ) && + assert(stringify("01:01:01+01X01:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01X01:01 is not a valid ISO-8601 format, illegal offset time at index 12)") + ) + ) && + assert(stringify("01:01:01+01:0").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:0 is not a valid ISO-8601 format, illegal offset time at index 12)") + ) + ) && + assert(stringify("01:01:01+01:X1:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:X1:01 is not a valid ISO-8601 format, expected digit at index 12)") + ) + ) && + assert(stringify("01:01:01+01:0X:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:0X:01 is not a valid ISO-8601 format, expected digit at index 13)") + ) + ) && + assert(stringify("01:01:01+01:60:01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 13)") + ) + ) && + assert(stringify("01:01:01+01:01X01").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:01X01 is not a valid ISO-8601 format, illegal offset time at index 15)") + ) + ) && + assert(stringify("01:01:01+01:01:0").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:01:0 is not a valid ISO-8601 format, illegal offset time at index 15)") + ) + ) && + assert(stringify("01:01:01+01:01:X1").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:01:X1 is not a valid ISO-8601 format, expected digit at index 15)") + ) + ) && + assert(stringify("01:01:01+01:01:0X").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:01:0X is not a valid ISO-8601 format, expected digit at index 16)") + ) + ) && + assert(stringify("01:01:01+01:01:60").fromSExpr[OffsetTime])( + isLeft( + equalTo("(01:01:01+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 16)") + ) + ) + }, + test("Period") { + assert(stringify("").fromSExpr[Period])( + isLeft(equalTo("( is not a valid ISO-8601 format, illegal period at index 0)")) + ) && + assert(stringify("X").fromSExpr[Period])( + isLeft(equalTo("(X is not a valid ISO-8601 format, expected 'P' or '-' at index 0)")) + ) && + assert(stringify("P").fromSExpr[Period])( + isLeft(equalTo("(P is not a valid ISO-8601 format, illegal period at index 1)")) + ) && + assert(stringify("-").fromSExpr[Period])( + isLeft(equalTo("(- is not a valid ISO-8601 format, illegal period at index 1)")) + ) && + assert(stringify("PXY").fromSExpr[Period])( + isLeft(equalTo("(PXY is not a valid ISO-8601 format, expected '-' or digit at index 1)")) + ) && + assert(stringify("P-").fromSExpr[Period])( + isLeft(equalTo("(P- is not a valid ISO-8601 format, illegal period at index 2)")) + ) && + assert(stringify("P-XY").fromSExpr[Period])( + isLeft(equalTo("(P-XY is not a valid ISO-8601 format, expected digit at index 2)")) + ) && + assert(stringify("P1XY").fromSExpr[Period])( + isLeft( + equalTo("(P1XY is not a valid ISO-8601 format, expected 'Y' or 'M' or 'W' or 'D' or digit at index 2)") + ) + ) && + assert(stringify("P2147483648Y").fromSExpr[Period])( + isLeft(equalTo("(P2147483648Y is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P21474836470Y").fromSExpr[Period])( + isLeft(equalTo("(P21474836470Y is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P-2147483649Y").fromSExpr[Period])( + isLeft(equalTo("(P-2147483649Y is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P2147483648M").fromSExpr[Period])( + isLeft(equalTo("(P2147483648M is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P21474836470M").fromSExpr[Period])( + isLeft(equalTo("(P21474836470M is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P-2147483649M").fromSExpr[Period])( + isLeft(equalTo("(P-2147483649M is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P2147483648W").fromSExpr[Period])( + isLeft(equalTo("(P2147483648W is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P21474836470W").fromSExpr[Period])( + isLeft(equalTo("(P21474836470W is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P-2147483649W").fromSExpr[Period])( + isLeft(equalTo("(P-2147483649W is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P2147483648D").fromSExpr[Period])( + isLeft(equalTo("(P2147483648D is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P21474836470D").fromSExpr[Period])( + isLeft(equalTo("(P21474836470D is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P-2147483649D").fromSExpr[Period])( + isLeft(equalTo("(P-2147483649D is not a valid ISO-8601 format, illegal period at index 11)")) + ) && + assert(stringify("P1YXM").fromSExpr[Period])( + isLeft(equalTo("(P1YXM is not a valid ISO-8601 format, expected '-' or digit at index 3)")) + ) && + assert(stringify("P1Y-XM").fromSExpr[Period])( + isLeft(equalTo("(P1Y-XM is not a valid ISO-8601 format, expected digit at index 4)")) + ) && + assert(stringify("P1Y1XM").fromSExpr[Period])( + isLeft(equalTo("(P1Y1XM is not a valid ISO-8601 format, expected 'M' or 'W' or 'D' or digit at index 4)")) + ) && + assert(stringify("P1Y2147483648M").fromSExpr[Period])( + isLeft(equalTo("(P1Y2147483648M is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y21474836470M").fromSExpr[Period])( + isLeft(equalTo("(P1Y21474836470M is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y-2147483649M").fromSExpr[Period])( + isLeft(equalTo("(P1Y-2147483649M is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y2147483648W").fromSExpr[Period])( + isLeft(equalTo("(P1Y2147483648W is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y21474836470W").fromSExpr[Period])( + isLeft(equalTo("(P1Y21474836470W is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y-2147483649W").fromSExpr[Period])( + isLeft(equalTo("(P1Y-2147483649W is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y2147483648D").fromSExpr[Period])( + isLeft(equalTo("(P1Y2147483648D is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y21474836470D").fromSExpr[Period])( + isLeft(equalTo("(P1Y21474836470D is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y-2147483649D").fromSExpr[Period])( + isLeft(equalTo("(P1Y-2147483649D is not a valid ISO-8601 format, illegal period at index 13)")) + ) && + assert(stringify("P1Y1MXW").fromSExpr[Period])( + isLeft(equalTo("(P1Y1MXW is not a valid ISO-8601 format, expected '\"' or '-' or digit at index 5)")) + ) && + assert(stringify("P1Y1M-XW").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M-XW is not a valid ISO-8601 format, expected digit at index 6)")) + ) && + assert(stringify("P1Y1M1XW").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M1XW is not a valid ISO-8601 format, expected 'W' or 'D' or digit at index 6)")) + ) && + assert(stringify("P1Y1M306783379W").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M306783379W is not a valid ISO-8601 format, illegal period at index 14)")) + ) && + assert(stringify("P1Y1M3067833790W").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M3067833790W is not a valid ISO-8601 format, illegal period at index 14)")) + ) && + assert(stringify("P1Y1M-306783379W").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M-306783379W is not a valid ISO-8601 format, illegal period at index 15)")) + ) && + assert(stringify("P1Y1M2147483648D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M2147483648D is not a valid ISO-8601 format, illegal period at index 15)")) + ) && + assert(stringify("P1Y1M21474836470D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M21474836470D is not a valid ISO-8601 format, illegal period at index 15)")) + ) && + assert(stringify("P1Y1M-2147483649D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M-2147483649D is not a valid ISO-8601 format, illegal period at index 15)")) + ) && + assert(stringify("P1Y1M1WXD").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M1WXD is not a valid ISO-8601 format, expected '\"' or '-' or digit at index 7)")) + ) && + assert(stringify("P1Y1M1W-XD").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M1W-XD is not a valid ISO-8601 format, expected digit at index 8)")) + ) && + assert(stringify("P1Y1M1W1XD").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M1W1XD is not a valid ISO-8601 format, expected 'D' or digit at index 8)")) + ) && + assert(stringify("P1Y1M306783378W8D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M306783378W8D is not a valid ISO-8601 format, illegal period at index 16)")) + ) && + assert(stringify("P1Y1M-306783378W-8D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M-306783378W-8D is not a valid ISO-8601 format, illegal period at index 18)")) + ) && + assert(stringify("P1Y1M1W2147483647D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M1W2147483647D is not a valid ISO-8601 format, illegal period at index 17)")) + ) && + assert(stringify("P1Y1M-1W-2147483648D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M-1W-2147483648D is not a valid ISO-8601 format, illegal period at index 19)")) + ) && + assert(stringify("P1Y1M0W2147483648D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M0W2147483648D is not a valid ISO-8601 format, illegal period at index 17)")) + ) && + assert(stringify("P1Y1M0W21474836470D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M0W21474836470D is not a valid ISO-8601 format, illegal period at index 17)")) + ) && + assert(stringify("P1Y1M0W-2147483649D").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M0W-2147483649D is not a valid ISO-8601 format, illegal period at index 17)")) + ) && + assert(stringify("P1Y1M1W1DX").fromSExpr[Period])( + isLeft(equalTo("(P1Y1M1W1DX is not a valid ISO-8601 format, illegal period at index 9)")) + ) + }, + test("Year") { + assert(stringify("").fromSExpr[Year])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal year at index 0)") + ) + ) && + assert(stringify("2").fromSExpr[Year])( + isLeft( + equalTo("(2 is not a valid ISO-8601 format, illegal year at index 0)") + ) + ) && + assert(stringify("22").fromSExpr[Year])( + isLeft( + equalTo("(22 is not a valid ISO-8601 format, illegal year at index 0)") + ) + ) && + assert(stringify("222").fromSExpr[Year])( + isLeft( + equalTo("(222 is not a valid ISO-8601 format, illegal year at index 0)") + ) + ) && + assert(stringify("X020").fromSExpr[Year])( + isLeft( + equalTo("(X020 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") + ) + ) && + assert(stringify("2X20").fromSExpr[Year])( + isLeft( + equalTo("(2X20 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("20X0").fromSExpr[Year])( + isLeft( + equalTo("(20X0 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("202X").fromSExpr[Year])( + isLeft( + equalTo("(202X is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+X0000").fromSExpr[Year])( + isLeft( + equalTo("(+X0000 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+1X000").fromSExpr[Year])( + isLeft( + equalTo("(+1X000 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+10X00").fromSExpr[Year])( + isLeft( + equalTo("(+10X00 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+100X0").fromSExpr[Year])( + isLeft( + equalTo("(+100X0 is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+1000X").fromSExpr[Year])( + isLeft( + equalTo("(+1000X is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+10000X").fromSExpr[Year])( + isLeft( + equalTo("(+10000X is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("+100000X").fromSExpr[Year])( + isLeft( + equalTo("(+100000X is not a valid ISO-8601 format, expected digit at index 7)") + ) + ) && + assert(stringify("+1000000X").fromSExpr[Year])( + isLeft( + equalTo("(+1000000X is not a valid ISO-8601 format, expected digit at index 8)") + ) + ) && + assert(stringify("+1000000000").fromSExpr[Year])( + isLeft( + equalTo("(+1000000000 is not a valid ISO-8601 format, illegal year at index 10)") + ) + ) && + assert(stringify("-1000000000").fromSExpr[Year])( + isLeft( + equalTo("(-1000000000 is not a valid ISO-8601 format, illegal year at index 10)") + ) + ) && + assert(stringify("-0000").fromSExpr[Year])( + isLeft( + equalTo("(-0000 is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) && + assert(stringify("10000").fromSExpr[Year])( + isLeft( + equalTo("(10000 is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) + }, + test("YearMonth") { + assert(stringify("").fromSExpr[YearMonth])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal year month at index 0)") + ) + ) && + assert(stringify("2020").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020 is not a valid ISO-8601 format, illegal year month at index 0)") + ) + ) && + assert(stringify("2020-0").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020-0 is not a valid ISO-8601 format, illegal year month at index 5)") + ) + ) && + assert(stringify("2020-012").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020-012 is not a valid ISO-8601 format, illegal year month at index 7)") + ) + ) && + assert(stringify("X020-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(X020-01 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") + ) + ) && + assert(stringify("2X20-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(2X20-01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("20X0-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(20X0-01 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("202X-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(202X-01 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("2020X01").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020X01 is not a valid ISO-8601 format, expected '-' at index 4)") + ) + ) && + assert(stringify("2020-X1").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020-X1 is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("2020-0X").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020-0X is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("+X0000-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+X0000-01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+1X000-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+1X000-01 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+10X00-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+10X00-01 is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+100X0-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+100X0-01 is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+1000X-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+1000X-01 is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+10000X-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+10000X-01 is not a valid ISO-8601 format, expected '-' or digit at index 6)") + ) + ) && + assert(stringify("+100000X-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+100000X-01 is not a valid ISO-8601 format, expected '-' or digit at index 7)") + ) + ) && + assert(stringify("+1000000X-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+1000000X-01 is not a valid ISO-8601 format, expected '-' or digit at index 8)") + ) + ) && + assert(stringify("+1000000000-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(+1000000000-01 is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-1000000000-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(-1000000000-01 is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-0000-01").fromSExpr[YearMonth])( + isLeft( + equalTo("(-0000-01 is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) && + assert(stringify("+10000").fromSExpr[YearMonth])( + isLeft( + equalTo("(+10000 is not a valid ISO-8601 format, illegal year month at index 6)") + ) + ) && + assert(stringify("2020-00").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020-00 is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-13").fromSExpr[YearMonth])( + isLeft( + equalTo("(2020-13 is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) + }, + test("ZonedDateTime") { + assert(stringify("").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal zoned date time at index 0)") + ) + ) && + assert(stringify("2020").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020 is not a valid ISO-8601 format, illegal zoned date time at index 0)") + ) + ) && + assert(stringify("2020-0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-0 is not a valid ISO-8601 format, illegal zoned date time at index 5)") + ) + ) && + assert(stringify("2020-01-0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal zoned date time at index 8)") + ) + ) && + assert(stringify("2020-01-01T0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal zoned date time at index 11)") + ) + ) && + assert(stringify("2020-01-01T01:0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal zoned date time at index 14)") + ) + ) && + assert(stringify("X020-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(X020-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") + ) + ) && + assert(stringify("2X20-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2X20-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("20X0-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(20X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("202X-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(202X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("2020X01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020X01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 4)") + ) + ) && + assert(stringify("2020-X1-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-X1-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("2020-0X-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-0X-01T01:01Z is not a valid ISO-8601 format, expected digit at index 6)") + ) + ) && + assert(stringify("2020-01X01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01X01T01:01Z is not a valid ISO-8601 format, expected '-' at index 7)") + ) + ) && + assert(stringify("2020-01-X1T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-X1T01:01Z is not a valid ISO-8601 format, expected digit at index 8)") + ) + ) && + assert(stringify("2020-01-0XT01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-0XT01:01Z is not a valid ISO-8601 format, expected digit at index 9)") + ) + ) && + assert(stringify("2020-01-01X01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01X01:01Z is not a valid ISO-8601 format, expected 'T' at index 10)") + ) + ) && + assert(stringify("2020-01-01TX1:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") + ) + ) && + assert(stringify("2020-01-01T0X:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") + ) + ) && + assert(stringify("2020-01-01T24:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") + ) + ) && + assert(stringify("2020-01-01T01X01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") + ) + ) && + assert(stringify("2020-01-01T01:X1").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") + ) + ) && + assert(stringify("2020-01-01T01:0X").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:60").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") + ) + ) && + assert(stringify("2020-01-01T01:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01X").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal zoned date time at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:X1Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:X1Z is not a valid ISO-8601 format, expected digit at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01:0XZ").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:0XZ is not a valid ISO-8601 format, expected digit at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:60Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:60Z is not a valid ISO-8601 format, illegal second at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:012").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01.").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01. is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01.X").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01.X is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01.123456789X").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01.123456789X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 29)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01ZX").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01ZX is not a valid ISO-8601 format, expected '[' at index 20)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+X1:01:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+X1:01:01 is not a valid ISO-8601 format, expected digit at index 20)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+0 is not a valid ISO-8601 format, illegal zoned date time at index 20)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+0X:01:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+0X:01:01 is not a valid ISO-8601 format, expected digit at index 21)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+19:01:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 21)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01X01:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01X01:01 is not a valid ISO-8601 format, expected '[' at index 22)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:0 is not a valid ISO-8601 format, illegal zoned date time at index 23)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:X1:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:X1:01 is not a valid ISO-8601 format, expected digit at index 23)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:0X:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:0X:01 is not a valid ISO-8601 format, expected digit at index 24)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:60:01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 24)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01X01").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:01X01 is not a valid ISO-8601 format, expected '[' at index 25)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:0").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:01:0 is not a valid ISO-8601 format, illegal zoned date time at index 26)" + ) + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:X1").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:01:X1 is not a valid ISO-8601 format, expected digit at index 26)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:0X").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:01:0X is not a valid ISO-8601 format, expected digit at index 27)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:01X").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01:01+01:01:01X is not a valid ISO-8601 format, expected '[' at index 28)") + ) + ) && + assert(stringify("2020-01-01T01:01:01+01:01:60").fromSExpr[ZonedDateTime])( + isLeft( + equalTo( + "(2020-01-01T01:01:01+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 27)" + ) + ) + ) && + assert(stringify("+X0000-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+X0000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+1X000-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+1X000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+10X00-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+10X00-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") + ) + ) && + assert(stringify("+100X0-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+100X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+1000X-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+1000X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+10000X-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+10000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 6)") + ) + ) && + assert(stringify("+100000X-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+100000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 7)") + ) + ) && + assert(stringify("+1000000X-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+1000000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 8)") + ) + ) && + assert(stringify("+1000000000-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-1000000000-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(-1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") + ) + ) && + assert(stringify("-0000-01-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(-0000-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 4)") + ) + ) && + assert(stringify("+10000").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(+10000 is not a valid ISO-8601 format, illegal zoned date time at index 6)") + ) + ) && + assert(stringify("2020-00-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-00-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-13-01T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-13-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") + ) + ) && + assert(stringify("2020-01-00T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-00T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-01-32T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-02-30T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-02-30T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-03-32T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-03-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-04-31T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-04-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-05-32T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-05-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-06-31T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-06-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-07-32T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-07-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-08-32T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-08-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-09-31T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-09-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-10-32T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-10-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-11-31T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-11-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-12-32T01:01Z").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-12-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") + ) + ) && + assert(stringify("2020-01-01T01:01Z[").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01Z[ is not a valid ISO-8601 format, illegal zoned date time at index 17)") + ) + ) && + assert(stringify("2020-01-01T01:01Z[X]").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01Z[X] is not a valid ISO-8601 format, illegal zoned date time at index 18)") + ) + ) && + assert(stringify("2020-01-01T01:01Z[GMT]X").fromSExpr[ZonedDateTime])( + isLeft( + equalTo("(2020-01-01T01:01Z[GMT]X is not a valid ISO-8601 format, illegal zoned date time at index 22)") + ) + ) + }, + test("ZoneId") { + assert(stringify("America/New York").fromSExpr[ZoneId])( + isLeft(equalTo("(America/New York is not a valid ISO-8601 format, illegal zone id at index 0)")) + ) && + assert(stringify("Solar_System/Mars").fromSExpr[ZoneId])( + isLeft(equalTo("(Solar_System/Mars is not a valid ISO-8601 format, illegal zone id at index 0)")) + ) + }, + test("ZoneOffset") { + assert(stringify("").fromSExpr[ZoneOffset])( + isLeft( + equalTo("( is not a valid ISO-8601 format, illegal zone offset at index 0)") + ) + ) && + assert(stringify("X").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 0)") + ) + ) && + assert(stringify("+X1:01:01").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+X1:01:01 is not a valid ISO-8601 format, expected digit at index 1)") + ) + ) && + assert(stringify("+0").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+0 is not a valid ISO-8601 format, illegal zone offset at index 1)") + ) + ) && + assert(stringify("+0X:01:01").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+0X:01:01 is not a valid ISO-8601 format, expected digit at index 2)") + ) + ) && + assert(stringify("+19:01:01").fromSExpr[ZoneOffset])( + isLeft( + equalTo( + "(+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 2)" + ) + ) + ) && + assert(stringify("+01X01:01").fromSExpr[ZoneOffset])( + isLeft( + equalTo( + "(+01X01:01 is not a valid ISO-8601 format, illegal zone offset at index 4)" + ) + ) + ) && + assert(stringify("+01:0").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+01:0 is not a valid ISO-8601 format, illegal zone offset at index 4)") + ) + ) && + assert(stringify("+01:X1:01").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+01:X1:01 is not a valid ISO-8601 format, expected digit at index 4)") + ) + ) && + assert(stringify("+01:0X:01").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+01:0X:01 is not a valid ISO-8601 format, expected digit at index 5)") + ) + ) && + assert(stringify("+01:60:01").fromSExpr[ZoneOffset])( + isLeft( + equalTo( + "(+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 5)" + ) + ) + ) && + assert(stringify("+01:01X01").fromSExpr[ZoneOffset])( + isLeft( + equalTo( + "(+01:01X01 is not a valid ISO-8601 format, illegal zone offset at index 7)" + ) + ) + ) && + assert(stringify("+01:01:0").fromSExpr[ZoneOffset])( + isLeft( + equalTo( + "(+01:01:0 is not a valid ISO-8601 format, illegal zone offset at index 7)" + ) + ) + ) && + assert(stringify("+01:01:X1").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+01:01:X1 is not a valid ISO-8601 format, expected digit at index 7)") + ) + ) && + assert(stringify("+01:01:0X").fromSExpr[ZoneOffset])( + isLeft( + equalTo("(+01:01:0X is not a valid ISO-8601 format, expected digit at index 8)") + ) + ) && + assert(stringify("+01:01:60").fromSExpr[ZoneOffset])( + isLeft( + equalTo( + "(+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 8)" + ) + ) + ) + } + ) + ) +} diff --git a/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/RoundTripSpec.scala b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/RoundTripSpec.scala index 959bbab9..692614b7 100644 --- a/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/RoundTripSpec.scala +++ b/zio-morphir-sexpr/shared/src/test/scala/zio/morphir/sexpr/RoundTripSpec.scala @@ -1,19 +1,101 @@ package zio.morphir.sexpr +import zio.morphir.sexpr.Gens._ +import zio.morphir.sexpr._ import zio.morphir.testing.ZioBaseSpec import zio.test._ import zio.test.Assertion._ import zio.test.TestAspect._ -object RoundTripSpec extends ZioBaseSpec { +import java.time._ +object RoundTripSpec extends ZioBaseSpec { def spec = suite("RoundTrip")( - testM("booleans") { - check(Gen.boolean)(assertRoundtrips) - } @@ ignore @@ tag("Not sure what is broken here"), - testM("char") { - check(Gen.anyChar)(assertRoundtrips) - } + suite("primitives")( + testM("bigInt") { + check(genBigInteger)(assertRoundtrips) + } @@ samples(1000), + testM("bigDecimal") { + check(genBigDecimal)(assertRoundtrips) + } @@ samples(1000), + testM("boolean") { + check(Gen.boolean)(assertRoundtrips) + } @@ samples(1000), + testM("byte") { + check(Gen.anyByte)(assertRoundtrips) + } @@ samples(1000), + testM("char") { + check(Gen.anyChar)(assertRoundtrips) + } @@ samples(1000), + testM("double") { + check(Gen.anyDouble)(assertRoundtrips) + } @@ samples(1000), + testM("float") { + check(Gen.anyFloat)(assertRoundtrips) + } @@ samples(1000), + testM("int") { + check(Gen.anyInt)(assertRoundtrips) + } @@ samples(1000), + testM("long") { + check(Gen.anyLong)(assertRoundtrips) + } @@ samples(1000), + testM("short") { + check(Gen.anyShort)(assertRoundtrips) + } @@ samples(1000), + testM("string") { + check(Gen.anyString)(assertRoundtrips) + } @@ samples(1000) + ), + suite("java.time")( + testM("Duration") { + check(genDuration)(assertRoundtrips) + } @@ samples(1000), + testM("Instant") { + check(genInstant)(assertRoundtrips) + } @@ samples(1000), + testM("LocalDate") { + check(genLocalDate)(assertRoundtrips) + } @@ samples(1000), + testM("LocalDateTime") { + check(genLocalDateTime)(assertRoundtrips) + } @@ samples(1000), + testM("LocalTime") { + check(genLocalTime)(assertRoundtrips) + } @@ samples(1000), + testM("Month") { + check(genMonth)(assertRoundtrips) + } @@ samples(1000), + testM("MonthDay") { + check(genMonthDay)(assertRoundtrips) + } @@ samples(1000), + testM("OffsetDateTime") { + check(genOffsetDateTime)(assertRoundtrips) + } @@ samples(1000), + testM("OffsetTime") { + check(genOffsetTime)(assertRoundtrips) + } @@ samples(1000), + testM("Period") { + check(genPeriod)(assertRoundtrips) + } @@ samples(1000), + testM("Year") { + check(genYear)(assertRoundtrips) + } @@ samples(1000), + testM("YearMonth") { + check(genYearMonth)(assertRoundtrips) + } @@ samples(1000), + testM("ZoneId") { + check(genZoneId)(assertRoundtrips[ZoneId]) + }, + testM("ZoneOffset") { + check(genZoneOffset)(assertRoundtrips[ZoneOffset]) + }, + testM("ZonedDateTime") { + check(genZonedDateTime)(assertRoundtrips) + } @@ samples(1000) + ), + testM("UUID") { + check(Gen.anyUUID)(assertRoundtrips) + } @@ samples(1000), ) private def assertRoundtrips[A: SExprEncoder: SExprDecoder](a: A) =