diff --git a/ast/jvm/src/test/scala/jawn/ast/AstCheckPlatform.scala b/ast/jvm/src/test/scala/jawn/ast/AstCheckPlatform.scala index de5e3f8e..a9c94fc2 100644 --- a/ast/jvm/src/test/scala/jawn/ast/AstCheckPlatform.scala +++ b/ast/jvm/src/test/scala/jawn/ast/AstCheckPlatform.scala @@ -56,6 +56,14 @@ private[jawn] trait AstCheckPlatform { self: AstCheck => p0 && p1 } + property("string/charSequence parsing") = forAll { (value: JValue) => + val s = CanonicalRenderer.render(value) + val j1 = JParser.parseFromString(s) + val cs = java.nio.CharBuffer.wrap(s.toCharArray) + val j2 = JParser.parseFromCharSequence(cs) + Prop(j1 == j2 && j1.## == j2.##) + } + import AsyncParser.SingleValue property("async parsing") = forAll { (v: JValue) => diff --git a/ast/jvm/src/test/scala/jawn/ast/AstTestPlatform.scala b/ast/jvm/src/test/scala/jawn/ast/AstTestPlatform.scala index a6eb18f0..679f1ff2 100644 --- a/ast/jvm/src/test/scala/jawn/ast/AstTestPlatform.scala +++ b/ast/jvm/src/test/scala/jawn/ast/AstTestPlatform.scala @@ -23,8 +23,11 @@ package org.typelevel.jawn package ast import org.scalacheck.Prop +import Prop.{forAll, forAllNoShrink} -import Prop.forAll +import scala.util.Try + +import ArbitraryUtil.expNotationNums private[jawn] trait AstTestPlatform { self: AstTest => @@ -37,4 +40,10 @@ private[jawn] trait AstTestPlatform { self: AstTest => ) } + expNotationNums.foreach { (expForm: (String, Double)) => + property(s".asDouble ${expForm._1}") = Prop( + JParser.parseUnsafe(expForm._1).getDouble == Try(JParser.parseUnsafe(expForm._1).asDouble).toOption && + JParser.parseUnsafe(expForm._1).asDouble == expForm._2 + ) + } } diff --git a/ast/native/src/test/scala/jawn/ast/AstCheckPlatform.scala b/ast/native/src/test/scala/jawn/ast/AstCheckPlatform.scala index 352051c8..b425dcf7 100644 --- a/ast/native/src/test/scala/jawn/ast/AstCheckPlatform.scala +++ b/ast/native/src/test/scala/jawn/ast/AstCheckPlatform.scala @@ -22,4 +22,19 @@ package org.typelevel.jawn package ast -private[jawn] trait AstCheckPlatform +import org.typelevel.jawn.ast.ArbitraryUtil.arbitraryJValue + +import org.scalacheck.Prop +import org.scalacheck.Prop.forAll + +private[jawn] trait AstCheckPlatform { self: AstCheck => + + // Rendering/parsing numbers on JS isn't always idempotent + property("string/charSequence parsing") = forAll { (value: JValue) => + val s = CanonicalRenderer.render(value) + val j1 = JParser.parseFromString(s) + val cs = java.nio.CharBuffer.wrap(s.toCharArray) + val j2 = JParser.parseFromCharSequence(cs) + Prop(j1 == j2 && j1.## == j2.##) + } +} diff --git a/ast/shared/src/main/scala/jawn/ast/JValue.scala b/ast/shared/src/main/scala/jawn/ast/JValue.scala index 59c9afc3..e7b9e030 100644 --- a/ast/shared/src/main/scala/jawn/ast/JValue.scala +++ b/ast/shared/src/main/scala/jawn/ast/JValue.scala @@ -25,6 +25,7 @@ package ast import java.lang.Double.{isInfinite, isNaN} import scala.collection.mutable import scala.reflect.ClassTag +import scala.util.Try import scala.util.hashing.MurmurHash3 class WrongValueException(e: String, g: String) extends Exception(s"expected $e, got $g") @@ -223,28 +224,33 @@ case class DoubleNum(n: Double) extends JNum { case class DeferLong(s: String) extends JNum { - lazy val n: Long = util.parseLongUnsafe(s) + lazy val nOpt: Option[Long] = Try(util.parseLong(s)).toOption + lazy val n: Long = nOpt.getOrElse(throw new InvalidNumException(s)) - final override def getInt: Option[Int] = Some(n.toInt) - final override def getLong: Option[Long] = Some(n) - final override def getDouble: Option[Double] = Some(n.toDouble) + final override def getInt: Option[Int] = nOpt.map(_.toInt) + final override def getLong: Option[Long] = nOpt + final override def getDouble: Option[Double] = nOpt.map(_.toDouble) final override def getBigInt: Option[BigInt] = Some(BigInt(s)) final override def getBigDecimal: Option[BigDecimal] = Some(BigDecimal(s)) - final override def asInt: Int = n.toInt - final override def asLong: Long = n - final override def asDouble: Double = n.toDouble + final override def asInt: Int = nOpt.map(_.toInt).getOrElse(throw new InvalidNumException(s)) + final override def asLong: Long = nOpt.getOrElse(throw new InvalidNumException(s)) + final override def asDouble: Double = nOpt.map(_.toDouble).getOrElse(throw new InvalidNumException(s)) final override def asBigInt: BigInt = BigInt(s) final override def asBigDecimal: BigDecimal = BigDecimal(s) - final override def hashCode: Int = n.## + final override def hashCode: Int = if (nOpt.isEmpty) s.## else nOpt.get.## final override def equals(that: Any): Boolean = - that match { - case LongNum(n2) => n == n2 - case DoubleNum(n2) => JNum.hybridEq(n, n2) - case jn: DeferLong => n == jn.asLong - case jn: DeferNum => JNum.hybridEq(n, jn.asDouble) + (nOpt, that) match { + // JNum with same string representation and type will behave the same way + case (None, that: DeferLong) => this.s == that.s + case (None, _) => false + case (_, None) => false + case (Some(n), LongNum(n2)) => n == n2 + case (Some(n), DoubleNum(n2)) => JNum.hybridEq(n, n2) + case (Some(n), jn: DeferLong) => n == jn.asLong + case (Some(n), jn: DeferNum) => JNum.hybridEq(n, jn.asDouble) case _ => false } } @@ -254,13 +260,13 @@ case class DeferNum(s: String) extends JNum { lazy val n: Double = java.lang.Double.parseDouble(s) final override def getInt: Option[Int] = Some(n.toInt) - final override def getLong: Option[Long] = Some(util.parseLongUnsafe(s)) + final override def getLong: Option[Long] = Some(n.toLong) final override def getDouble: Option[Double] = Some(n) final override def getBigInt: Option[BigInt] = Some(BigDecimal(s).toBigInt) final override def getBigDecimal: Option[BigDecimal] = Some(BigDecimal(s)) final override def asInt: Int = n.toInt - final override def asLong: Long = util.parseLongUnsafe(s) + final override def asLong: Long = n.toLong final override def asDouble: Double = n final override def asBigInt: BigInt = BigDecimal(s).toBigInt final override def asBigDecimal: BigDecimal = BigDecimal(s) @@ -271,7 +277,9 @@ case class DeferNum(s: String) extends JNum { that match { case LongNum(n2) => JNum.hybridEq(n2, n) case DoubleNum(n2) => n == n2 - case jn: DeferLong => JNum.hybridEq(jn.asLong, n) + case jn: DeferLong => + try JNum.hybridEq(jn.asLong, n) + catch { case _: InvalidNumException => false } case jn: DeferNum => n == jn.asDouble case _ => false } diff --git a/ast/shared/src/test/scala/jawn/ArbitraryUtil.scala b/ast/shared/src/test/scala/jawn/ArbitraryUtil.scala index 83280bcd..205dfba7 100644 --- a/ast/shared/src/test/scala/jawn/ArbitraryUtil.scala +++ b/ast/shared/src/test/scala/jawn/ArbitraryUtil.scala @@ -62,4 +62,15 @@ object ArbitraryUtil { implicit lazy val arbitraryJValue: Arbitrary[JValue] = Arbitrary(jvalue()) + + // Valid JSON numbers with an exact double representation and in the Long range + + implicit lazy val expNotationNums: List[(String, Double)] = List( + ("2e3", 2e3), + ("2.5e0", 2.5e0), + ("2e+3", 2e+3), + ("2.5e-1", 2.5e-1), + ("9.223372036854776e18", 9.223372036854776e18), + ("-9.223372036854776e+18", -9.223372036854776e18) + ) } diff --git a/ast/shared/src/test/scala/jawn/AstTest.scala b/ast/shared/src/test/scala/jawn/AstTest.scala index e7adc509..83def44f 100644 --- a/ast/shared/src/test/scala/jawn/AstTest.scala +++ b/ast/shared/src/test/scala/jawn/AstTest.scala @@ -23,8 +23,8 @@ package org.typelevel.jawn package ast import org.scalacheck.{Prop, Properties} -import scala.util.{Success, Try} +import scala.util.{Success, Try} import ArbitraryUtil._ import Prop.forAll @@ -62,6 +62,13 @@ class AstTest extends Properties("AstTest") with AstTestPlatform { ) } + expNotationNums.foreach { (expForm: (String, Double)) => + property(s".asInt ${expForm._1}") = Prop( + JParser.parseUnsafe(expForm._1).getInt == Try(JParser.parseUnsafe(expForm._1).asInt).toOption && + JParser.parseUnsafe(expForm._1).asInt == expForm._2.intValue() + ) + } + property(".getLong") = forAll { (n: Long) => Prop( JNum(n).getLong == Some(n) && @@ -69,6 +76,13 @@ class AstTest extends Properties("AstTest") with AstTestPlatform { ) } + expNotationNums.foreach { (expForm: (String, Double)) => + property(s".asLong ${expForm._1}") = Prop( + JParser.parseUnsafe(expForm._1).getLong == Try(JParser.parseUnsafe(expForm._1).asLong).toOption && + JParser.parseUnsafe(expForm._1).asLong == expForm._2.longValue() + ) + } + property(".getBigInt") = forAll { (n: BigInt) => Prop( JNum(n.toString).getBigInt == Some(n) && @@ -76,6 +90,13 @@ class AstTest extends Properties("AstTest") with AstTestPlatform { ) } + expNotationNums.foreach { (expForm: (String, Double)) => + property(s".asBigInt ${expForm._1}") = Prop( + JParser.parseUnsafe(expForm._1).getBigInt == Try(JParser.parseUnsafe(expForm._1).asBigInt).toOption && + JParser.parseUnsafe(expForm._1).asBigInt == BigDecimal(expForm._2).toBigInt + ) + } + property(".getBigDecimal") = forAll { (n: BigDecimal) => if (Try(BigDecimal(n.toString)) == Success(n)) Prop( @@ -85,4 +106,11 @@ class AstTest extends Properties("AstTest") with AstTestPlatform { else Prop(true) } + + expNotationNums.foreach { (expForm: (String, Double)) => + property(s".asBigDecimal ${expForm._1}") = Prop( + JParser.parseUnsafe(expForm._1).getBigDecimal == Try(JParser.parseUnsafe(expForm._1).asBigDecimal).toOption && + JParser.parseUnsafe(expForm._1).asBigDecimal == BigDecimal(expForm._2) + ) + } }