diff --git a/core/shared/src/main/scala/org/virtuslab/yaml/YamlDecoder.scala b/core/shared/src/main/scala/org/virtuslab/yaml/YamlDecoder.scala index 13cd05345..770331038 100644 --- a/core/shared/src/main/scala/org/virtuslab/yaml/YamlDecoder.scala +++ b/core/shared/src/main/scala/org/virtuslab/yaml/YamlDecoder.scala @@ -85,27 +85,52 @@ object YamlDecoder extends YamlDecoderCompanionCrossCompat { ) implicit def forInt: YamlDecoder[Int] = YamlDecoder { case s @ ScalarNode(value, _) => - Try(java.lang.Integer.decode(value.replaceAll("_", "")).toInt).toEither.left + val normalizedValue = + if (value.startsWith("0o")) value.stripPrefix("0o").prepended('0') else value + + Try(java.lang.Integer.decode(normalizedValue.replaceAll("_", "")).toInt).toEither.left .map(ConstructError.from(_, "Int", s)) } implicit def forLong: YamlDecoder[Long] = YamlDecoder { case s @ ScalarNode(value, _) => - Try(java.lang.Long.decode(value.replaceAll("_", "")).toLong).toEither.left + val normalizedValue = + if (value.startsWith("0o")) value.stripPrefix("0o").prepended('0') else value + + Try(java.lang.Long.decode(normalizedValue.replaceAll("_", "")).toLong).toEither.left .map(ConstructError.from(_, "Long", s)) } implicit def forDouble: YamlDecoder[Double] = YamlDecoder { case s @ ScalarNode(value, _) => - Try(java.lang.Double.parseDouble(value.replaceAll("_", ""))).toEither.left - .map(ConstructError.from(_, "Double", s)) + val lowercased = value.toLowerCase + if (lowercased.endsWith("inf")) { + if (value.startsWith("-")) Right(Double.NegativeInfinity) + else Right(Double.PositiveInfinity) + } else if (lowercased.endsWith("nan")) { + Right(Double.NaN) + } else { + Try(java.lang.Double.parseDouble(value.replaceAll("_", ""))).toEither.left + .map(ConstructError.from(_, "Double", s)) + } } implicit def forFloat: YamlDecoder[Float] = YamlDecoder { case s @ ScalarNode(value, _) => - Try(java.lang.Float.parseFloat(value.replaceAll("_", ""))).toEither.left - .map(ConstructError.from(_, "Float", s)) + val lowercased = value.toLowerCase + if (lowercased.endsWith("inf")) { + if (value.startsWith("-")) Right(Float.NegativeInfinity) + else Right(Float.PositiveInfinity) + } else if (lowercased.endsWith("nan")) { + Right(Float.NaN) + } else { + Try(java.lang.Float.parseFloat(value.replaceAll("_", ""))).toEither.left + .map(ConstructError.from(_, "Float", s)) + } } implicit def forShort: YamlDecoder[Short] = YamlDecoder { case s @ ScalarNode(value, _) => - Try(java.lang.Short.decode(value.replaceAll("_", "")).toShort).toEither.left + val normalizedValue = + if (value.startsWith("0o")) value.stripPrefix("0o").prepended('0') else value + + Try(java.lang.Short.decode(normalizedValue.replaceAll("_", "")).toShort).toEither.left .map(ConstructError.from(_, "Short", s)) } diff --git a/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala b/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala index 4a276c0da..1f19a5f27 100644 --- a/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala +++ b/core/shared/src/test/scala-3/org/virtuslab/yaml/decoder/DecoderSuite.scala @@ -360,6 +360,60 @@ class DecoderSuite extends munit.FunSuite: assertEquals(foo, Right(List(Some(Foo(1, "1")), None))) } + test("issue 222 - parse edge cases of booleans floats doubles and integers") { + case class Data( + booleans: List[Boolean], + integers: List[Int], + floats: List[Float], + `also floats`: List[Float], + `also doubles`: List[Double] + ) derives YamlCodec + + val yaml = """booleans: [ true, True, false, FALSE ] + |integers: [ 0, 0o7, 0x3A, -19 ] + |floats: [ + | 0., -0.0, .5, +12e03, -2E+05 ] + |also floats: [ + | .inf, -.Inf, +.INF, .NAN, .nan, .NaN] + |also doubles: [ + | .inf, -.Inf, +.INF, .NAN, .nan, .NaN]""".stripMargin + + val expected = Data( + booleans = List(true, true, false, false), + integers = List(0, 7, 58, -19), + floats = List(0.0f, -0.0f, 0.5f, 12000.0f, -200000.0f), + `also floats` = List( + Float.PositiveInfinity, + Float.NegativeInfinity, + Float.PositiveInfinity, + Float.NaN, + Float.NaN, + Float.NaN + ), + `also doubles` = List( + Double.PositiveInfinity, + Double.NegativeInfinity, + Double.PositiveInfinity, + Double.NaN, + Double.NaN, + Double.NaN + ) + ) + + yaml.as[Data] match + case Left(error: YamlError) => throw error + case Right(data) => + assertEquals(data.booleans, expected.booleans) + assertEquals(data.integers, expected.integers) + assertEquals(data.floats, expected.floats) + data.`also floats`.zipAll(expected.`also floats`, 0f, 0f).foreach { case (a, b) => + assertEqualsFloat(a, b, 0f) + } + data.`also doubles`.zipAll(expected.`also doubles`, 0.0d, 0.0d).foreach { case (a, b) => + assertEqualsDouble(a, b, 0.0d) + } + } + test("issue 281 - parse multiline string") { case class Data(description: String) derives YamlCodec