diff --git a/benchmark/jvm/src/test/scala/scalajson/ast/PrivateBenchmark.scala b/benchmark/jvm/src/test/scala/scalajson/ast/EqualsHashcodeBenchmark.scala similarity index 78% rename from benchmark/jvm/src/test/scala/scalajson/ast/PrivateBenchmark.scala rename to benchmark/jvm/src/test/scala/scalajson/ast/EqualsHashcodeBenchmark.scala index 939ac6d..4401d23 100644 --- a/benchmark/jvm/src/test/scala/scalajson/ast/PrivateBenchmark.scala +++ b/benchmark/jvm/src/test/scala/scalajson/ast/EqualsHashcodeBenchmark.scala @@ -3,10 +3,7 @@ package scalajson.ast import benchmark.Generators import org.scalameter.Bench -/** - * Created by matthewdedetrich on 17/10/16. - */ -object PrivateBenchmark extends Bench.ForkedTime { +object EqualsHashcodeBenchmark extends Bench.ForkedTime { performance of "privateMethods" in { measure method "hashcode" in { diff --git a/benchmark/jvm/src/test/scala/scalajson/ast/JNumberConversionBenchmark.scala b/benchmark/jvm/src/test/scala/scalajson/ast/JNumberConversionBenchmark.scala new file mode 100644 index 0000000..11f11b2 --- /dev/null +++ b/benchmark/jvm/src/test/scala/scalajson/ast/JNumberConversionBenchmark.scala @@ -0,0 +1,89 @@ +package scalajson.ast + +import org.scalameter.{Bench, Gen} + +object JNumberConversionBenchmark extends Bench.ForkedTime { + def intString: Gen[String] = + for { + size <- Gen.range("seed")(300000, 1500000, 300000) + } yield { + size.toString + } + + def floatString: Gen[String] = + for { + a <- Gen.range("seed")(300000, 1500000, 300000) + b <- Gen.range("seed")(300000, 1500000, 300000) + } yield { + s"$a.$b".toFloat.toString + } + + private val intConstructedFlag = NumberFlags.intConstructed + private val floatConstructedFlag = NumberFlags.floatConstructed + + performance of "intBitFlagCheckSuccess" in { + using(intString) in { value: String => + if ((intConstructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + } + + performance of "intManualCheckSuccess" in { + using(intString) in { value: String => + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + performance of "floatBitFlagCheckSuccess" in { + using(floatString) in { value: String => + if ((floatConstructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + + } + } + } + + performance of "floatManualCheckSuccess" in { + using(floatString) in { value: String => + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + + } + } +} diff --git a/js/src/main/scala-2.10/scalajson.ast/JValue.scala b/js/src/main/scala-2.10/scalajson.ast/JValue.scala index df9cacc..5383e2c 100644 --- a/js/src/main/scala-2.10/scalajson.ast/JValue.scala +++ b/js/src/main/scala-2.10/scalajson.ast/JValue.scala @@ -50,15 +50,20 @@ final case class JString(value: String) extends JValue { } object JNumber { - def apply(value: Int): JNumber = new JNumber(value.toString) + def apply(value: Int): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) - def apply(value: Integer): JNumber = new JNumber(value.toString) + def apply(value: Integer): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) - def apply(value: Long): JNumber = new JNumber(value.toString) + def apply(value: Long): JNumber = + new JNumber(value.toString)(NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = new JNumber(value.toString()) + def apply(value: BigInt): JNumber = + new JNumber(value.toString())(NumberFlags.bigIntConstructed) - def apply(value: BigDecimal): JNumber = new JNumber(value.toString()) + def apply(value: BigDecimal): JNumber = + new JNumber(value.toString())(NumberFlags.bigDecimalConstructed) /** * @param value @@ -67,7 +72,7 @@ object JNumber { def apply(value: Double): JValue = value match { case n if n.isNaN => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.doubleConstructed) } /** @@ -77,12 +82,12 @@ object JNumber { def apply(value: Float): JValue = value match { case n if java.lang.Float.isNaN(n) => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.floatConstructed) } def fromString(value: String): Option[JNumber] = value match { - case jNumberRegex(_ *) => Some(new JNumber(value)) + case jNumberRegex(_ *) => Some(new JNumber(value)(0)) case _ => None } @@ -97,15 +102,9 @@ object JNumber { */ // Due to a restriction in Scala 2.10, we cant override/replace the default apply method // generated by the compiler even when the constructor itself is marked private -final class JNumber private[ast] (val value: String) extends JValue { - - /** - * Javascript specification for numbers specify a [[scala.Double]], so this is the default export method to `Javascript` - * - * @param value - */ - def this(value: Double) = this(value.toString) - +final class JNumber private[ast] (val value: String)( + private[ast] val constructedFlag: Int) + extends JValue { override def toUnsafe: unsafe.JValue = unsafe.JNumber(value) override def toJsAny: js.Any = value.toDouble match { @@ -143,9 +142,85 @@ final class JNumber private[ast] (val value: String) extends JValue { def copy(value: String): JNumber = value match { - case jNumberRegex(_ *) => new JNumber(value) + case jNumberRegex(_ *) => new JNumber(value)(0) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } + } } /** Represents a JSON Boolean value, which can either be a diff --git a/js/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala b/js/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala index d1b7ca8..9424972 100644 --- a/js/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala +++ b/js/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala @@ -4,7 +4,6 @@ package unsafe import scalajson.ast import scalajson.ast._ import scala.scalajs.js -import scala.scalajs.js.annotation.JSExport /** Represents a JSON Value which may be invalid. Internally uses mutable * collections when its desirable to do so, for performance and other reasons @@ -56,21 +55,28 @@ final case class JString(value: String) extends JValue { } object JNumber { - def apply(value: Int): JNumber = JNumber(value.toString) + def apply(value: Int): JNumber = + JNumber(value.toString, NumberFlags.intConstructed) - def apply(value: Long): JNumber = JNumber(value.toString) + def apply(value: Long): JNumber = + JNumber(value.toString, NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = JNumber(value.toString) + def apply(value: BigInt): JNumber = + JNumber(value.toString, NumberFlags.bigIntConstructed) - def apply(value: BigDecimal): JNumber = JNumber(value.toString) + def apply(value: BigDecimal): JNumber = + JNumber(value.toString, NumberFlags.bigDecimalConstructed) - def apply(value: Float): JNumber = JNumber(value.toString) + def apply(value: Float): JNumber = + JNumber(value.toString, NumberFlags.floatConstructed) - def apply(value: Double): JNumber = JNumber(value.toString) + def apply(value: Double): JNumber = + JNumber(value.toString, NumberFlags.doubleConstructed) - def apply(value: Integer): JNumber = JNumber(value.toString) + def apply(value: Integer): JNumber = + JNumber(value.toString, NumberFlags.intConstructed) - def apply(value: Array[Char]): JNumber = JNumber(new String(value)) + def apply(value: Array[Char]): JNumber = JNumber(new String(value), (0)) } /** Represents a JSON number value. @@ -84,10 +90,11 @@ object JNumber { * @author Matthew de Detrich */ // JNumber is internally represented as a string, to improve performance -final case class JNumber(value: String) extends JValue { +final case class JNumber(value: String, constructedFlag: Int = 0) + extends JValue { override def toStandard: ast.JValue = value match { - case jNumberRegex(_ *) => new ast.JNumber(value) + case jNumberRegex(_ *) => new ast.JNumber(value)(constructedFlag) case _ => throw new NumberFormatException(value) } @@ -100,6 +107,99 @@ final case class JNumber(value: String) extends JValue { case n if n.isInfinity => null case n => n } + + override def equals(obj: scala.Any): Boolean = { + obj match { + case jNumber: JNumber => jNumber.value == this.value + case _ => false + } + } + + override def hashCode(): Int = value.## + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + try { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } catch { + case _: NumberFormatException => None + } + } + } } /** Represents a JSON Boolean value, which can either be a @@ -232,7 +332,7 @@ final case class JObject(value: js.Array[JField] = js.Array()) extends JValue { case unsafe.JNull => unsafe.JNull.## case unsafe.JString(s) => s.## case unsafe.JBoolean(b) => b.## - case unsafe.JNumber(i) => i.## + case unsafe.JNumber(i, _) => i.## case unsafe.JArray(a) => a.## case unsafe.JObject(obj) => obj.## } @@ -304,7 +404,7 @@ final case class JArray(value: js.Array[JValue] = js.Array()) extends JValue { case unsafe.JNull => unsafe.JNull.## case unsafe.JString(s) => s.## case unsafe.JBoolean(b) => b.## - case unsafe.JNumber(i) => i.## + case unsafe.JNumber(i, _) => i.## case unsafe.JArray(a) => a.## case unsafe.JObject(obj) => obj.## } diff --git a/js/src/main/scala/scalajson/ast/JValue.scala b/js/src/main/scala/scalajson/ast/JValue.scala index 87b638e..b4f2edd 100644 --- a/js/src/main/scala/scalajson/ast/JValue.scala +++ b/js/src/main/scala/scalajson/ast/JValue.scala @@ -50,15 +50,20 @@ final case class JString(value: String) extends JValue { } object JNumber { - def apply(value: Int): JNumber = new JNumber(value.toString) + def apply(value: Int): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) - def apply(value: Integer): JNumber = new JNumber(value.toString) + def apply(value: Integer): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) - def apply(value: Long): JNumber = new JNumber(value.toString) + def apply(value: Long): JNumber = + new JNumber(value.toString)(NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = new JNumber(value.toString()) + def apply(value: BigInt): JNumber = + new JNumber(value.toString())(NumberFlags.bigIntConstructed) - def apply(value: BigDecimal): JNumber = new JNumber(value.toString()) + def apply(value: BigDecimal): JNumber = + new JNumber(value.toString())(NumberFlags.bigDecimalConstructed) /** * @param value @@ -67,7 +72,7 @@ object JNumber { def apply(value: Double): JValue = value match { case n if n.isNaN => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.doubleConstructed) } /** @@ -77,7 +82,7 @@ object JNumber { def apply(value: Float): JValue = value match { case n if java.lang.Float.isNaN(n) => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.floatConstructed) } def apply(value: String): Option[JNumber] = @@ -85,7 +90,7 @@ object JNumber { def fromString(value: String): Option[JNumber] = value match { - case jNumberRegex(_*) => Some(new JNumber(value)) + case jNumberRegex(_*) => Some(new JNumber(value)(0)) case _ => None } } @@ -96,16 +101,19 @@ object JNumber { * * @author Matthew de Detrich */ -final case class JNumber private[ast] (value: String) extends JValue { +final case class JNumber private[ast] (value: String)( + private[ast] val constructedFlag: Int) + extends JValue { /** * Javascript specification for numbers specify a [[scala.Double]], so this is the default export method to `Javascript` * * @param value */ - def this(value: Double) = this(value.toString) + def this(value: Double) = this(value.toString)(NumberFlags.doubleConstructed) - override def toUnsafe: unsafe.JValue = unsafe.JNumber(value) + override def toUnsafe: unsafe.JValue = + new unsafe.JNumber(value, constructedFlag) override def toJsAny: js.Any = value.toDouble match { case n if n.isNaN => null @@ -125,9 +133,93 @@ final case class JNumber private[ast] (value: String) extends JValue { def copy(value: String): JNumber = value match { - case jNumberRegex(_*) => new JNumber(value) + case jNumberRegex(_*) => new JNumber(value)(0) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + try { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } catch { + case _: NumberFormatException => None + } + } + } } /** Represents a JSON Boolean value, which can either be a diff --git a/js/src/main/scala/scalajson/ast/unsafe/JValue.scala b/js/src/main/scala/scalajson/ast/unsafe/JValue.scala index 0ceb84b..3ac031d 100644 --- a/js/src/main/scala/scalajson/ast/unsafe/JValue.scala +++ b/js/src/main/scala/scalajson/ast/unsafe/JValue.scala @@ -54,21 +54,28 @@ final case class JString(value: String) extends JValue { } object JNumber { - def apply(value: Int): JNumber = JNumber(value.toString) + def apply(value: Int): JNumber = + new JNumber(value.toString, NumberFlags.intConstructed) - def apply(value: Long): JNumber = JNumber(value.toString) + def apply(value: Long): JNumber = + new JNumber(value.toString, NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = JNumber(value.toString) + def apply(value: BigInt): JNumber = + new JNumber(value.toString, NumberFlags.bigIntConstructed) - def apply(value: BigDecimal): JNumber = JNumber(value.toString) + def apply(value: BigDecimal): JNumber = + new JNumber(value.toString, NumberFlags.bigDecimalConstructed) - def apply(value: Float): JNumber = JNumber(value.toString) + def apply(value: Float): JNumber = + new JNumber(value.toString, NumberFlags.floatConstructed) - def apply(value: Double): JNumber = JNumber(value.toString) + def apply(value: Double): JNumber = + new JNumber(value.toString, NumberFlags.doubleConstructed) - def apply(value: Integer): JNumber = JNumber(value.toString) + def apply(value: Integer): JNumber = + new JNumber(value.toString, NumberFlags.intConstructed) - def apply(value: Array[Char]): JNumber = JNumber(new String(value)) + def apply(value: Array[Char]): JNumber = new JNumber(new String(value), 0) } /** Represents a JSON number value. @@ -82,22 +89,113 @@ object JNumber { * @author Matthew de Detrich */ // JNumber is internally represented as a string, to improve performance -final case class JNumber(value: String) extends JValue { +final case class JNumber(value: String, constructedFlag: Int = 0) + extends JValue { override def toStandard: ast.JValue = value match { - case jNumberRegex(_*) => new ast.JNumber(value) + case jNumberRegex(_*) => new ast.JNumber(value)(constructedFlag) case _ => throw new NumberFormatException(value) } - def this(value: Double) = { - this(value.toString) - } - override def toJsAny: js.Any = value.toDouble match { case n if n.isNaN => null case n if n.isInfinity => null case n => n } + + override def equals(obj: scala.Any): Boolean = { + obj match { + case jNumber: JNumber => jNumber.value == this.value + case _ => false + } + } + + override def hashCode(): Int = value.## + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + try { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } catch { + case _: NumberFormatException => None + } + + } + } } /** Represents a JSON Boolean value, which can either be a @@ -228,12 +326,12 @@ final case class JObject(value: js.Array[JField] = js.Array()) extends JValue { else { result = 31 * result + elem.field.## elem.value match { - case unsafe.JNull => unsafe.JNull.## - case unsafe.JString(s) => s.## - case unsafe.JBoolean(b) => b.## - case unsafe.JNumber(i) => i.## - case unsafe.JArray(a) => a.## - case unsafe.JObject(obj) => obj.## + case unsafe.JNull => unsafe.JNull.## + case unsafe.JString(s) => s.## + case unsafe.JBoolean(b) => b.## + case unsafe.JNumber(i, _) => i.## + case unsafe.JArray(a) => a.## + case unsafe.JObject(obj) => obj.## } }) index += 1 @@ -300,12 +398,12 @@ final case class JArray(value: js.Array[JValue] = js.Array()) extends JValue { result = 31 * result + (if (elem == null) 0 else { elem match { - case unsafe.JNull => unsafe.JNull.## - case unsafe.JString(s) => s.## - case unsafe.JBoolean(b) => b.## - case unsafe.JNumber(i) => i.## - case unsafe.JArray(a) => a.## - case unsafe.JObject(obj) => obj.## + case unsafe.JNull => unsafe.JNull.## + case unsafe.JString(s) => s.## + case unsafe.JBoolean(b) => b.## + case unsafe.JNumber(i, _) => i.## + case unsafe.JArray(a) => a.## + case unsafe.JObject(obj) => obj.## } }) index += 1 diff --git a/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala b/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala index d65381f..f54dd6c 100644 --- a/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala +++ b/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala @@ -40,11 +40,14 @@ final case class JString(value: String) extends JValue { * return a JNull */ object JNumber { - def apply(value: Int): JNumber = new JNumber(value.toString) + def apply(value: Int): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) - def apply(value: Long): JNumber = new JNumber(value.toString) + def apply(value: Long): JNumber = + new JNumber(value.toString)(NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = new JNumber(value.toString) + def apply(value: BigInt): JNumber = + new JNumber(value.toString)(NumberFlags.bigIntConstructed) /** * @param value @@ -53,10 +56,11 @@ object JNumber { def apply(value: Float): JValue = value match { case n if java.lang.Float.isNaN(n) => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.floatConstructed) } - def apply(value: BigDecimal): JNumber = new JNumber(value.toString()) + def apply(value: BigDecimal): JNumber = + new JNumber(value.toString())(NumberFlags.bigDecimalConstructed) /** * @param value @@ -65,17 +69,18 @@ object JNumber { def apply(value: Double): JValue = value match { case n if n.isNaN => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.doubleConstructed) } - def apply(value: Integer): JNumber = new JNumber(value.toString) + def apply(value: Integer): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) def apply(value: Array[Char]): Option[JNumber] = fromString(new String(value)) def fromString(value: String): Option[JNumber] = value match { - case jNumberRegex(_ *) => Some(new JNumber(value)) + case jNumberRegex(_ *) => Some(new JNumber(value)(0)) case _ => None } @@ -90,8 +95,11 @@ object JNumber { */ // Due to a restriction in Scala 2.10, we cant override/replace the default apply method // generated by the compiler even when the constructor itself is marked private -final class JNumber private[ast] (val value: String) extends JValue { - override def toUnsafe: unsafe.JValue = unsafe.JNumber(value) +final class JNumber private[ast] (val value: String)( + private val constructedFlag: Int) + extends JValue { + override def toUnsafe: unsafe.JValue = + new unsafe.JNumber(value, constructedFlag) override def equals(obj: Any): Boolean = obj match { @@ -122,9 +130,93 @@ final class JNumber private[ast] (val value: String) extends JValue { def copy(value: String): JNumber = value match { - case jNumberRegex(_ *) => new JNumber(value) + case jNumberRegex(_ *) => new JNumber(value)(0) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + try { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } catch { + case _: NumberFormatException => None + } + } + } } /** Represents a JSON Boolean value, which can either be a diff --git a/jvm/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala b/jvm/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala index 3437473..2582a37 100644 --- a/jvm/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala +++ b/jvm/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala @@ -42,21 +42,30 @@ final case class JString(value: String) extends JValue { } object JNumber { - def apply(value: Int): JNumber = JNumber(value.toInt.toString) + def apply(value: Int): JNumber = + new JNumber(value.toInt.toString, NumberFlags.intConstructed) - def apply(value: Long): JNumber = JNumber(value.toString) + def apply(value: Long): JNumber = + new JNumber(value.toString, NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = JNumber(value.toString) + def apply(value: BigInt): JNumber = + new JNumber(value.toString, NumberFlags.bigIntConstructed) - def apply(value: BigDecimal): JNumber = JNumber(value.toString) + def apply(value: BigDecimal): JNumber = + new JNumber(value.toString, NumberFlags.bigDecimalConstructed) - def apply(value: Float): JNumber = JNumber(value.toString) + def apply(value: Float): JNumber = + new JNumber(value.toString, NumberFlags.floatConstructed) - def apply(value: Double): JNumber = JNumber(value.toString) + def apply(value: Double): JNumber = + new JNumber(value.toString, NumberFlags.doubleConstructed) - def apply(value: Integer): JNumber = JNumber(value.toString) + def apply(value: Integer): JNumber = + new JNumber(value.toString, NumberFlags.intConstructed) - def apply(value: Array[Char]): JNumber = JNumber(new String(value)) + def apply(value: Array[Char]): JNumber = new JNumber(new String(value), 0) + + def apply(value: String): JNumber = new JNumber(value, 0) } /** Represents a JSON number value. @@ -70,12 +79,107 @@ object JNumber { * @author Matthew de Detrich */ // JNumber is internally represented as a string, to improve performance -final case class JNumber(value: String) extends JValue { +final case class JNumber(value: String, constructedFlag: Int = 0) + extends JValue { override def toStandard: ast.JValue = value match { - case jNumberRegex(_ *) => new ast.JNumber(value) + case jNumberRegex(_ *) => new ast.JNumber(value)(constructedFlag) case _ => throw new NumberFormatException(value) } + + override def equals(obj: scala.Any): Boolean = { + obj match { + case jNumber: JNumber => jNumber.value == this.value + case _ => false + } + } + + override def hashCode(): Int = value.## + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + try { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } catch { + case _: NumberFormatException => None + } + } + } } /** Represents a JSON Boolean value, which can either be a diff --git a/jvm/src/main/scala/scalajson/ast/JValue.scala b/jvm/src/main/scala/scalajson/ast/JValue.scala index 463102d..2724ee5 100644 --- a/jvm/src/main/scala/scalajson/ast/JValue.scala +++ b/jvm/src/main/scala/scalajson/ast/JValue.scala @@ -40,11 +40,14 @@ final case class JString(value: String) extends JValue { * return a JNull */ object JNumber { - def apply(value: Int): JNumber = new JNumber(value.toString) + def apply(value: Int): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) - def apply(value: Long): JNumber = new JNumber(value.toString) + def apply(value: Long): JNumber = + new JNumber(value.toString)(NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = new JNumber(value.toString) + def apply(value: BigInt): JNumber = + new JNumber(value.toString)(NumberFlags.bigIntConstructed) /** * @param value @@ -53,10 +56,11 @@ object JNumber { def apply(value: Float): JValue = value match { case n if java.lang.Float.isNaN(n) => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.floatConstructed) } - def apply(value: BigDecimal): JNumber = new JNumber(value.toString()) + def apply(value: BigDecimal): JNumber = + new JNumber(value.toString())(NumberFlags.bigDecimalConstructed) /** * @param value @@ -65,10 +69,11 @@ object JNumber { def apply(value: Double): JValue = value match { case n if n.isNaN => JNull case n if n.isInfinity => JNull - case _ => new JNumber(value.toString) + case _ => new JNumber(value.toString)(NumberFlags.doubleConstructed) } - def apply(value: Integer): JNumber = new JNumber(value.toString) + def apply(value: Integer): JNumber = + new JNumber(value.toString)(NumberFlags.intConstructed) def apply(value: Array[Char]): Option[JNumber] = fromString(new String(value)) @@ -77,7 +82,7 @@ object JNumber { def fromString(value: String): Option[JNumber] = value match { - case jNumberRegex(_*) => Some(new JNumber(value)) + case jNumberRegex(_*) => Some(new JNumber(value)(0)) case _ => None } } @@ -88,8 +93,11 @@ object JNumber { * * @author Matthew de Detrich */ -final case class JNumber private[ast] (value: String) extends JValue { - override def toUnsafe: unsafe.JValue = unsafe.JNumber(value) +final case class JNumber private[ast] (value: String)( + private[ast] val constructedFlag: Int) + extends JValue { + override def toUnsafe: unsafe.JValue = + new unsafe.JNumber(value, constructedFlag) override def equals(obj: Any): Boolean = obj match { @@ -103,9 +111,93 @@ final case class JNumber private[ast] (value: String) extends JValue { def copy(value: String): JNumber = value match { - case jNumberRegex(_*) => new JNumber(value) + case jNumberRegex(_*) => new JNumber(value)(0) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + try { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + try { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } catch { + case _: NumberFormatException => None + } + } + } } /** Represents a JSON Boolean value, which can either be a diff --git a/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala b/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala index 8fcc07c..93d7b1f 100644 --- a/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala +++ b/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala @@ -42,21 +42,30 @@ final case class JString(value: String) extends JValue { } object JNumber { - def apply(value: Int): JNumber = JNumber(value.toInt.toString) + def apply(value: Int): JNumber = + new JNumber(value.toInt.toString, NumberFlags.intConstructed) - def apply(value: Long): JNumber = JNumber(value.toString) + def apply(value: Long): JNumber = + new JNumber(value.toString, NumberFlags.longConstructed) - def apply(value: BigInt): JNumber = JNumber(value.toString) + def apply(value: BigInt): JNumber = + new JNumber(value.toString, NumberFlags.bigIntConstructed) - def apply(value: BigDecimal): JNumber = JNumber(value.toString) + def apply(value: BigDecimal): JNumber = + new JNumber(value.toString, NumberFlags.bigDecimalConstructed) - def apply(value: Float): JNumber = JNumber(value.toString) + def apply(value: Float): JNumber = + new JNumber(value.toString, NumberFlags.floatConstructed) - def apply(value: Double): JNumber = JNumber(value.toString) + def apply(value: Double): JNumber = + new JNumber(value.toString, NumberFlags.doubleConstructed) - def apply(value: Integer): JNumber = JNumber(value.toString) + def apply(value: Integer): JNumber = + new JNumber(value.toString, NumberFlags.intConstructed) - def apply(value: Array[Char]): JNumber = JNumber(new String(value)) + def apply(value: Array[Char]): JNumber = new JNumber(new String(value), 0) + + def unapply(arg: JNumber): Option[String] = Some(arg.value) } /** Represents a JSON number value. @@ -70,12 +79,101 @@ object JNumber { * @author Matthew de Detrich */ // JNumber is internally represented as a string, to improve performance -final case class JNumber(value: String) extends JValue { +final case class JNumber(value: String, constructedFlag: Int = 0) + extends JValue { + def isEmpty: Boolean = false + def get: String = value + override def toStandard: ast.JValue = value match { - case jNumberRegex(_*) => new ast.JNumber(value) + case jNumberRegex(_*) => new ast.JNumber(value)(constructedFlag) case _ => throw new NumberFormatException(value) } + + override def equals(obj: scala.Any): Boolean = { + obj match { + case jNumber: JNumber => jNumber.value == this.value + case _ => false + } + } + + override def hashCode(): Int = value.## + + def toInt: Option[Long] = { + if ((constructedFlag & NumberFlags.int) == NumberFlags.int) + Some(value.toInt) + else { + try { + val asInt = value.toInt + if (BigInt(value) == BigInt(asInt)) + Some(asInt) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toLong: Option[Long] = { + if ((constructedFlag & NumberFlags.long) == NumberFlags.long) + Some(value.toLong) + else { + try { + val asLong = value.toLong + if (BigInt(value) == BigInt(asLong)) + Some(asLong) + else + None + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigInt: Option[BigInt] = { + if ((constructedFlag & NumberFlags.bigInt) == NumberFlags.bigInt) + Some(BigInt(value)) + else { + try { + Some(BigInt(value)) + } catch { + case _: NumberFormatException => None + } + } + } + + def toBigDecimal: Option[BigDecimal] = { + try { + Some(BigDecimal(value)) + } catch { + case _: NumberFormatException => None + } + } + + def toFloat: Option[Float] = { + if ((constructedFlag & NumberFlags.float) == NumberFlags.float) + Some(value.toFloat) + else { + val asFloat = value.toFloat + if (BigDecimal(value) == BigDecimal(asFloat.toDouble)) + Some(asFloat) + else + None + } + } + + def toDouble: Option[Double] = { + if ((constructedFlag & NumberFlags.double) == NumberFlags.double) + Some(value.toDouble) + else { + val asDouble = value.toDouble + if (BigDecimal(value) == BigDecimal(asDouble)) + Some(asDouble) + else + None + } + } } /** Represents a JSON Boolean value, which can either be a diff --git a/shared/src/main/scala-2.10/scalajson.ast/package.scala b/shared/src/main/scala-2.10/scalajson.ast/package.scala index 3f6f7b3..8e56b38 100644 --- a/shared/src/main/scala-2.10/scalajson.ast/package.scala +++ b/shared/src/main/scala-2.10/scalajson.ast/package.scala @@ -4,6 +4,27 @@ import scala.util.matching.Regex package object ast { + // Bit flags that are used for storing how a number was constructed + + object NumberFlags { + @inline private[ast] final def int: Int = 1 + @inline private[ast] final def long: Int = 2 + @inline private[ast] final def bigInt: Int = 4 + @inline private[ast] final def bigDecimal: Int = 8 + @inline private[ast] final def float: Int = 16 + @inline private[ast] final def double: Int = 32 + + @inline private[ast] final val intConstructed + : Int = int | long | bigInt | bigDecimal + @inline private[ast] final val longConstructed + : Int = long | bigInt | bigDecimal + @inline private[ast] final val bigIntConstructed: Int = bigInt + @inline private[ast] final val bigDecimalConstructed: Int = bigDecimal + @inline private[ast] final val floatConstructed + : Int = float | double | bigDecimal + @inline private[ast] final val doubleConstructed: Int = double | bigDecimal + } + /** * A regex that will match any valid JSON number for unlimited * precision diff --git a/shared/src/main/scala/scalajson/ast/package.scala b/shared/src/main/scala/scalajson/ast/package.scala index 3f6f7b3..8e56b38 100644 --- a/shared/src/main/scala/scalajson/ast/package.scala +++ b/shared/src/main/scala/scalajson/ast/package.scala @@ -4,6 +4,27 @@ import scala.util.matching.Regex package object ast { + // Bit flags that are used for storing how a number was constructed + + object NumberFlags { + @inline private[ast] final def int: Int = 1 + @inline private[ast] final def long: Int = 2 + @inline private[ast] final def bigInt: Int = 4 + @inline private[ast] final def bigDecimal: Int = 8 + @inline private[ast] final def float: Int = 16 + @inline private[ast] final def double: Int = 32 + + @inline private[ast] final val intConstructed + : Int = int | long | bigInt | bigDecimal + @inline private[ast] final val longConstructed + : Int = long | bigInt | bigDecimal + @inline private[ast] final val bigIntConstructed: Int = bigInt + @inline private[ast] final val bigDecimalConstructed: Int = bigDecimal + @inline private[ast] final val floatConstructed + : Int = float | double | bigDecimal + @inline private[ast] final val doubleConstructed: Int = double | bigDecimal + } + /** * A regex that will match any valid JSON number for unlimited * precision diff --git a/shared/src/test/scala/specs/unsafe/Generators.scala b/shared/src/test/scala/specs/unsafe/Generators.scala index badbad7..dd43683 100644 --- a/shared/src/test/scala/specs/unsafe/Generators.scala +++ b/shared/src/test/scala/specs/unsafe/Generators.scala @@ -189,7 +189,7 @@ object Generators { case obj: scalajson.ast.unsafe.JObject => shrink(obj) case scalajson.ast.unsafe.JString(str) => shrink(str) map scalajson.ast.unsafe.JString - case scalajson.ast.unsafe.JNumber(num) => + case scalajson.ast.unsafe.JNumber(num, _) => shrink(num) map (x => scalajson.ast.unsafe.JNumber(x)) case scalajson.ast.unsafe.JNull | scalajson.ast.unsafe.JBoolean(_) => Stream.empty[scalajson.ast.unsafe.JValue]