diff --git a/README.md b/README.md index 78709ca..ee2d99f 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,28 @@ in an undefined manner. Do note that according to the JSON spec, whether to order keys for a `JObject` is not specified. Also note that `Map` disregards ordering for equality, however `Array`/`js.Array` equality takes ordering into account. +## Number conversions +ScalaJSON `JNumber` provides conversions to various number types with the following conventions + +* `toInt`: Safe conversion to `Int` which accounts for values such as `1.0` and `100.00e-2` (which both evaluate to `1`). +Also safely detects over/underflow. +* `toLong`: Safe conversion to `Long` which accounts for values such as `1.0` and `100.00e-2` (which both evaluate to `1`). +Also safely detects over/underflow. +* `toDouble`: Converts to a `Double` assuming the same semantics of `Double` (i.e. precision loss is expected). +* `toFloat`: Converts to a `Float` assuming the same semantics of `Float` (i.e. precision loss is expected). +* `toBigInt`: Converts to a `BigInt` which accounts for values such as `1.0` and `100.00e-2` (which evaluates to `1`). +Can construct a `BigInt`for as much as memory as the system has (if your system runs out of memory this is considered +undefined behaviour). +* `toBigDecimal`: Converts to a `BigDecimal` with all of the caveats of `BigDecimal` construction. The `BigDecimal` is +constructed with `MathContext.UNLIMITED` precision. + +With the `.toFloat` and `.toDouble` methods, if you don't want any loss in precision, its advisable to convert to +`BigDecimal` first and then work from there, i.e. when working with `Decimal`/`Float`, its implied that you will +have loss of precision. + +Remember that in all cases if these methods are not applicable, you can always use the `.value` field to get the +original string representation of the number. + ## Scala.js ScalaJSON also provides support for [Scala.js](https://github.com/scala-js/scala-js). The usage of Scala.js mirrors the usage of Scala on the JVM however Scala.js also implements @@ -103,6 +125,10 @@ underlying number (numbers in Javascript are represented as double precision flo You can use the `.value` method on a `scalajson.ast.JNumber`/`scalajson.ast.unsafe.JNumber` to get the raw string value as a solution to this problem. +Further, `toFloat` on `JNumber` (see [Number Conversions](#number-conversions) ) can have different semantics on Scala.js, depending on whether you have +strict-floats enabled in your application. Please see the [Scala.js semantics page](https://www.scala-js.org/doc/semantics.html) +for more information. + ## jNumberRegex `scalajson.JNumber` uses `jNumberRegex` to validate whether a number is a valid JSON number. One can use `jNumberRegex` explicitly if you want to use the validation that diff --git a/benchmark/jvm/src/test/scala/benchmark/Constants.scala b/benchmark/jvm/src/test/scala/benchmark/Constants.scala new file mode 100644 index 0000000..84b87b3 --- /dev/null +++ b/benchmark/jvm/src/test/scala/benchmark/Constants.scala @@ -0,0 +1,5 @@ +package benchmark + +object Constants { + @inline def innerLoopConstant: Int = 10000 +} diff --git a/benchmark/jvm/src/test/scala/benchmark/Generators.scala b/benchmark/jvm/src/test/scala/benchmark/Generators.scala index 6c6b255..3f4b63b 100644 --- a/benchmark/jvm/src/test/scala/benchmark/Generators.scala +++ b/benchmark/jvm/src/test/scala/benchmark/Generators.scala @@ -25,8 +25,9 @@ object Generators { def jNumber: Gen[ast.JNumber] = for { size <- Gen.range("seed")(300000, 1500000, 300000) + size2 <- Gen.range("seed")(300000, 1500000, 300000) } yield { - ast.JNumber(size) + ast.JNumber.fromString(s"$size.$size2").get } def jArray: Gen[ast.JArray] = diff --git a/benchmark/jvm/src/test/scala/benchmark/JNumber.scala b/benchmark/jvm/src/test/scala/benchmark/JNumber.scala index 9279499..b7ad0fb 100644 --- a/benchmark/jvm/src/test/scala/benchmark/JNumber.scala +++ b/benchmark/jvm/src/test/scala/benchmark/JNumber.scala @@ -7,7 +7,56 @@ object JNumber extends Bench.ForkedTime { performance of "JNumber" in { measure method "toFast" in { using(Generators.jNumber) in { jNumber => - jNumber.toUnsafe + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toUnsafe + i += 1 + } + } + } + measure method "toInt" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toInt + i += 1 + } + } + } + measure method "toLong" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toLong + i += 1 + } + } + } + measure method "toBigInt" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toBigInt + i += 1 + } + } + } + measure method "toBigDecimal" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toBigDecimal + i += 1 + } + } + } + measure method "toDouble" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toDouble + i += 1 + } } } } diff --git a/benchmark/jvm/src/test/scala/benchmark/unsafe/JNumber.scala b/benchmark/jvm/src/test/scala/benchmark/unsafe/JNumber.scala index 8cb306b..812a117 100644 --- a/benchmark/jvm/src/test/scala/benchmark/unsafe/JNumber.scala +++ b/benchmark/jvm/src/test/scala/benchmark/unsafe/JNumber.scala @@ -1,5 +1,6 @@ package benchmark.unsafe +import benchmark.Constants import org.scalameter.Bench object JNumber extends Bench.ForkedTime { @@ -10,5 +11,50 @@ object JNumber extends Bench.ForkedTime { jNumber.toStandard } } + measure method "toInt" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toInt + i += 1 + } + } + } + measure method "toLong" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toLong + i += 1 + } + } + } + measure method "toBigInt" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toBigInt + i += 1 + } + } + } + measure method "toBigDecimal" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toBigDecimal + i += 1 + } + } + } + measure method "toDouble" in { + using(Generators.jNumber) in { jNumber => + var i = 0 + while (i < Constants.innerLoopConstant) { + jNumber.toDouble + i += 1 + } + } + } } } 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..4ba776e 100644 --- a/js/src/main/scala-2.10/scalajson.ast/JValue.scala +++ b/js/src/main/scala-2.10/scalajson.ast/JValue.scala @@ -98,14 +98,6 @@ 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) - override def toUnsafe: unsafe.JValue = unsafe.JNumber(value) override def toJsAny: js.Any = value.toDouble match { @@ -146,6 +138,18 @@ final class JNumber private[ast] (val value: String) extends JValue { case jNumberRegex(_ *) => new JNumber(value) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** 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..59447e5 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 @@ -87,19 +87,27 @@ object JNumber { final case class JNumber(value: String) extends JValue { override def toStandard: ast.JValue = value match { - case jNumberRegex(_ *) => new ast.JNumber(value) - case _ => throw new NumberFormatException(value) + case jNumberRegex(_*) => new ast.JNumber(value) + 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.isNaN => null case n if n.isInfinity => null - case n => n + case n => n } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** Represents a JSON Boolean value, which can either be a @@ -229,11 +237,11 @@ 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.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.## } }) @@ -301,11 +309,11 @@ 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.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.## } }) diff --git a/js/src/main/scala/scalajson/ast/JValue.scala b/js/src/main/scala/scalajson/ast/JValue.scala index 87b638e..6f46c83 100644 --- a/js/src/main/scala/scalajson/ast/JValue.scala +++ b/js/src/main/scala/scalajson/ast/JValue.scala @@ -98,13 +98,6 @@ object JNumber { */ final case class JNumber private[ast] (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) - override def toUnsafe: unsafe.JValue = unsafe.JNumber(value) override def toJsAny: js.Any = value.toDouble match { @@ -128,6 +121,18 @@ final case class JNumber private[ast] (value: String) extends JValue { case jNumberRegex(_*) => new JNumber(value) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** 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..9d5eed8 100644 --- a/js/src/main/scala/scalajson/ast/unsafe/JValue.scala +++ b/js/src/main/scala/scalajson/ast/unsafe/JValue.scala @@ -89,15 +89,23 @@ final case class JNumber(value: String) extends JValue { 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 } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** Represents a JSON Boolean value, which can either be a 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..99cbfc1 100644 --- a/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala +++ b/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala @@ -125,6 +125,18 @@ final class JNumber private[ast] (val value: String) extends JValue { case jNumberRegex(_ *) => new JNumber(value) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** 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..e6046db 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 @@ -76,6 +76,18 @@ final case class JNumber(value: String) extends JValue { case jNumberRegex(_ *) => new ast.JNumber(value) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** 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..011eb72 100644 --- a/jvm/src/main/scala/scalajson/ast/JValue.scala +++ b/jvm/src/main/scala/scalajson/ast/JValue.scala @@ -106,6 +106,18 @@ final case class JNumber private[ast] (value: String) extends JValue { case jNumberRegex(_*) => new JNumber(value) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** 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..3a7c660 100644 --- a/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala +++ b/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala @@ -76,6 +76,18 @@ final case class JNumber(value: String) extends JValue { case jNumberRegex(_*) => new ast.JNumber(value) case _ => throw new NumberFormatException(value) } + + def toInt: Option[Int] = scalajson.ast.toInt(value) + + def toBigInt: Option[BigInt] = scalajson.ast.toBigInt(value) + + def toLong: Option[Long] = scalajson.ast.toLong(value) + + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) + + def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } /** 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..c837b2d 100644 --- a/shared/src/main/scala-2.10/scalajson.ast/package.scala +++ b/shared/src/main/scala-2.10/scalajson.ast/package.scala @@ -1,5 +1,7 @@ package scalajson +import java.math.MathContext + import scala.util.matching.Regex package object ast { @@ -343,4 +345,371 @@ package object ast { } } else pa == pb } + + @inline private[ast] def radix: Int = 10 + + private[ast] def toInt(value: String): Option[Int] = { + @inline def maxLengthConstant: Int = 10 + var limit: Int = -Integer.MAX_VALUE + var decimalFlag = false + var result: Int = 0 + var resultBigInt: BigInt = null + var negative = false + var multmin: Int = 0 + var char: Char = 0 + var i = 0 + var eFlag = false + var trailingZeroes: Int = 0 + var negativeEFlag: Boolean = false + var resultNegativeEFlag: Int = 0 + var digitLength: Int = 0 + multmin = limit / radix + if (value.charAt(0) == '-') { + limit = Integer.MIN_VALUE + negative = true + i += 1 + } + + while (i < value.length) { + char = value.charAt(i) + if (char == '.') + decimalFlag = true + else if ((char | 0x20) == 'e') { + eFlag = true + val charNext = value.charAt(i + 1) + if (charNext == '-') + negativeEFlag = true + if (negativeEFlag || charNext == '+') { + i += 1 + } + } else { + if (!(eFlag || decimalFlag)) + digitLength += 1 + + val digit = Character.digit(char, radix) + + if (digit == 0) + if (!decimalFlag) + trailingZeroes += 1 + else if (!decimalFlag) + trailingZeroes = 0 + + if (digit != 0 && (decimalFlag || eFlag)) + if (!negativeEFlag) + return None + else { + if (trailingZeroes != 0) { + resultNegativeEFlag *= radix + resultNegativeEFlag += digit + if (trailingZeroes >= resultNegativeEFlag) { + var i2: Int = 0 + while (i2 < resultNegativeEFlag) { + if (resultBigInt == null) + result = result / radix + else + resultBigInt = resultBigInt / radix + i2 += 1 + } + } else + return None + } + } + + val maxLenCheck = digitLength <= maxLengthConstant + + if (resultBigInt == null) { + if (result < multmin && maxLenCheck && !eFlag) + return None + } else { + if (resultBigInt < multmin && maxLenCheck && !eFlag) + return None + } + + if (!(digit == 0 && (decimalFlag || eFlag))) { + + if (!negativeEFlag) { + if (digitLength == maxLengthConstant - 1) { + var result2: Int = result + result2 *= radix + + if (!negative && result2 < 0 || negative && result2 > 0) { + resultBigInt = BigInt(result) + } + } + + if (resultBigInt == null) { + result *= radix + if (result < limit + digit && maxLenCheck) + return None + result -= digit + } else { + resultBigInt *= radix + if (resultBigInt < limit + digit && maxLenCheck) + return None + resultBigInt -= digit + } + } + } + } + i += 1 + } + if (resultBigInt == null) { + if (result < limit) + None + else { + if (negative) + Some(result) + else + Some(-result) + } + } else { + if (resultBigInt < limit) + None + else { + if (negative) + Some(resultBigInt.toInt) + else + Some(-resultBigInt.toInt) + } + } + } + + private[ast] def toLong(value: String): Option[Long] = { + @inline def maxLengthConstant: Int = 19 + var limit: Long = -Long.MaxValue + var decimalFlag = false + var result: Long = 0 + var resultBigInt: BigInt = null + var negative = false + var multmin: Long = 0 + var char: Char = 0 + var i = 0 + var eFlag = false + var trailingZeroes: Int = 0 + var negativeEFlag: Boolean = false + var resultNegativeEFlag: Long = 0 + var digitLength: Int = 0 + multmin = limit / radix + if (value.charAt(0) == '-') { + limit = Long.MinValue + negative = true + i += 1 + } + + while (i < value.length) { + char = value.charAt(i) + if (char == '.') + decimalFlag = true + else if ((char | 0x20) == 'e') { + eFlag = true + val charNext = value.charAt(i + 1) + if (charNext == '-') + negativeEFlag = true + if (negativeEFlag || charNext == '+') { + i += 1 + } + } else { + if (!(eFlag || decimalFlag)) + digitLength += 1 + + val digit = Character.digit(char, radix) + + if (digit == 0) + if (!decimalFlag) + trailingZeroes += 1 + else if (!decimalFlag) + trailingZeroes = 0 + + if (digit != 0 && (decimalFlag || eFlag)) + if (!negativeEFlag) + return None + else { + if (trailingZeroes != 0) { + resultNegativeEFlag *= radix + resultNegativeEFlag += digit + if (trailingZeroes >= resultNegativeEFlag) { + var i2: Int = 0 + while (i2 < resultNegativeEFlag) { + if (resultBigInt == null) + result = result / radix + else + resultBigInt = resultBigInt / radix + i2 += 1 + } + } else + return None + } + } + + val maxLenCheck = digitLength <= maxLengthConstant + + if (resultBigInt == null) { + if (result < multmin && maxLenCheck && !eFlag) + return None + } else { + if (resultBigInt < multmin && maxLenCheck && !eFlag) + return None + } + + if (!(digit == 0 && (decimalFlag || eFlag))) { + + if (!negativeEFlag) { + if (digitLength == maxLengthConstant - 1) { + var result2: Long = result + result2 *= radix + + if (!negative && result2 < 0 || negative && result2 > 0) { + resultBigInt = BigInt(result) + } + } + + if (resultBigInt == null) { + result *= radix + if (result < limit + digit && maxLenCheck) + return None + result -= digit + } else { + resultBigInt *= radix + if (resultBigInt < limit + digit && maxLenCheck) + return None + resultBigInt -= digit + } + } + } + } + i += 1 + } + if (resultBigInt == null) { + if (result < limit) + None + else { + if (negative) + Some(result) + else + Some(-result) + } + } else { + if (resultBigInt < limit) + None + else { + if (negative) + Some(resultBigInt.toLong) + else + Some(-resultBigInt.toLong) + } + } + } + + private[ast] def toBigInt(value: String): Option[BigInt] = { + var decimalFlag = false + @inline def maxLengthConstant: Int = 10 + var result: Int = 0 // assume that values are initially small when we convert to bigInt + var resultBigInt: BigInt = null + var negative = false + var char: Char = 0 + var i = 0 + var eFlag = false + var trailingZeroes: Int = 0 + var negativeEFlag: Boolean = false + var resultNegativeEFlag: Int = 0 + var digitLength: Int = 0 + if (value.charAt(0) == '-') { + negative = true + i += 1 + } + + while (i < value.length) { + char = value.charAt(i) + if (char == '.') + decimalFlag = true + else if ((char | 0x20) == 'e') { + eFlag = true + val charNext = value.charAt(i + 1) + if (charNext == '-') + negativeEFlag = true + if (negativeEFlag || charNext == '+') { + i += 1 + } + } else { + if (!(eFlag || decimalFlag)) + digitLength += 1 + + val digit = Character.digit(char, radix) + + if (digit == 0) + if (!decimalFlag) + trailingZeroes += 1 + else if (!decimalFlag) + trailingZeroes = 0 + + if (digit != 0 && (decimalFlag || eFlag)) + if (!negativeEFlag) + return None + else { + if (trailingZeroes != 0) { + resultNegativeEFlag *= radix + resultNegativeEFlag += digit + if (trailingZeroes >= resultNegativeEFlag) { + var i2: Int = 0 + while (i2 < resultNegativeEFlag) { + result = result / radix + i2 += 1 + } + } else + return None + } + } + + if (!(digit == 0 && (decimalFlag || eFlag))) { + + if (!negativeEFlag) { + if (digitLength == maxLengthConstant - 1) { + var result2: Long = result + result2 *= radix + + if (!negative && result2 < 0 || negative && result2 > 0) { + resultBigInt = BigInt(result) + } + } + + if (resultBigInt == null) { + result *= radix + result -= digit + } else { + resultBigInt *= radix + resultBigInt -= digit + } + } + } + } + i += 1 + } + + if (resultBigInt == null) { + if (negative) + Some(BigInt(result)) + else + Some(BigInt(-result)) + } else { + if (negative) + Some(resultBigInt) + else + Some(resultBigInt) + } + } + + @inline private[ast] def toDouble(value: String): Double = + value.toDouble + + @inline private[ast] def toFloat(value: String): Float = + value.toFloat + + private[ast] def toBigDecimal(value: String): Option[BigDecimal] = { + try { + Some(BigDecimal(value, MathContext.UNLIMITED)) + } catch { + case _: NumberFormatException | _: ArithmeticException => + None + } + } } diff --git a/shared/src/main/scala/scalajson/ast/package.scala b/shared/src/main/scala/scalajson/ast/package.scala index 3f6f7b3..bd335c2 100644 --- a/shared/src/main/scala/scalajson/ast/package.scala +++ b/shared/src/main/scala/scalajson/ast/package.scala @@ -1,5 +1,7 @@ package scalajson +import java.math.MathContext + import scala.util.matching.Regex package object ast { @@ -343,4 +345,370 @@ package object ast { } } else pa == pb } + + @inline private[ast] def radix: Int = 10 + + private[ast] def toInt(value: String): Option[Int] = { + @inline def maxLengthConstant: Int = 10 + var limit: Int = -Integer.MAX_VALUE + var decimalFlag = false + var result: Int = 0 + var resultBigInt: BigInt = null + var negative = false + var multmin: Int = 0 + var char: Char = 0 + var i = 0 + var eFlag = false + var trailingZeroes: Int = 0 + var negativeEFlag: Boolean = false + var resultNegativeEFlag: Int = 0 + var digitLength: Int = 0 + multmin = limit / radix + if (value.charAt(0) == '-') { + limit = Integer.MIN_VALUE + negative = true + i += 1 + } + + while (i < value.length) { + char = value.charAt(i) + if (char == '.') + decimalFlag = true + else if ((char | 0x20) == 'e') { + eFlag = true + val charNext = value.charAt(i + 1) + if (charNext == '-') + negativeEFlag = true + if (negativeEFlag || charNext == '+') { + i += 1 + } + } else { + if (!(eFlag || decimalFlag)) + digitLength += 1 + + val digit = Character.digit(char, radix) + + if (digit == 0) + if (!decimalFlag) + trailingZeroes += 1 + else if (!decimalFlag) + trailingZeroes = 0 + + if (digit != 0 && (decimalFlag || eFlag)) + if (!negativeEFlag) + return None + else { + if (trailingZeroes != 0) { + resultNegativeEFlag *= radix + resultNegativeEFlag += digit + if (trailingZeroes >= resultNegativeEFlag) { + var i2: Int = 0 + while (i2 < resultNegativeEFlag) { + if (resultBigInt == null) + result = result / radix + else + resultBigInt = resultBigInt / radix + i2 += 1 + } + } else + return None + } + } + + val maxLenCheck = digitLength <= maxLengthConstant + + if (resultBigInt == null) { + if (result < multmin && maxLenCheck && !eFlag) + return None + } else { + if (resultBigInt < multmin && maxLenCheck && !eFlag) + return None + } + + if (!(digit == 0 && (decimalFlag || eFlag))) { + + if (!negativeEFlag) { + if (digitLength == maxLengthConstant - 1) { + var result2: Int = result + result2 *= radix + + if (!negative && result2 < 0 || negative && result2 > 0) { + resultBigInt = BigInt(result) + } + } + + if (resultBigInt == null) { + result *= radix + if (result < limit + digit && maxLenCheck) + return None + result -= digit + } else { + resultBigInt *= radix + if (resultBigInt < limit + digit && maxLenCheck) + return None + resultBigInt -= digit + } + } + } + } + i += 1 + } + if (resultBigInt == null) { + if (result < limit) + None + else { + if (negative) + Some(result) + else + Some(-result) + } + } else { + if (resultBigInt < limit) + None + else { + if (negative) + Some(resultBigInt.toInt) + else + Some(-resultBigInt.toInt) + } + } + } + + private[ast] def toLong(value: String): Option[Long] = { + @inline def maxLengthConstant: Int = 19 + var limit: Long = -Long.MaxValue + var decimalFlag = false + var result: Long = 0 + var resultBigInt: BigInt = null + var negative = false + var multmin: Long = 0 + var char: Char = 0 + var i = 0 + var eFlag = false + var trailingZeroes: Int = 0 + var negativeEFlag: Boolean = false + var resultNegativeEFlag: Long = 0 + var digitLength: Int = 0 + multmin = limit / radix + if (value.charAt(0) == '-') { + limit = Long.MinValue + negative = true + i += 1 + } + + while (i < value.length) { + char = value.charAt(i) + if (char == '.') + decimalFlag = true + else if ((char | 0x20) == 'e') { + eFlag = true + val charNext = value.charAt(i + 1) + if (charNext == '-') + negativeEFlag = true + if (negativeEFlag || charNext == '+') { + i += 1 + } + } else { + if (!(eFlag || decimalFlag)) + digitLength += 1 + + val digit = Character.digit(char, radix) + + if (digit == 0) + if (!decimalFlag) + trailingZeroes += 1 + else if (!decimalFlag) + trailingZeroes = 0 + + if (digit != 0 && (decimalFlag || eFlag)) + if (!negativeEFlag) + return None + else { + if (trailingZeroes != 0) { + resultNegativeEFlag *= radix + resultNegativeEFlag += digit + if (trailingZeroes >= resultNegativeEFlag) { + var i2: Int = 0 + while (i2 < resultNegativeEFlag) { + if (resultBigInt == null) + result = result / radix + else + resultBigInt = resultBigInt / radix + i2 += 1 + } + } else + return None + } + } + + val maxLenCheck = digitLength <= maxLengthConstant + + if (resultBigInt == null) { + if (result < multmin && maxLenCheck && !eFlag) + return None + } else { + if (resultBigInt < multmin && maxLenCheck && !eFlag) + return None + } + + if (!(digit == 0 && (decimalFlag || eFlag))) { + + if (!negativeEFlag) { + if (digitLength == maxLengthConstant - 1) { + var result2: Long = result + result2 *= radix + + if (!negative && result2 < 0 || negative && result2 > 0) { + resultBigInt = BigInt(result) + } + } + + if (resultBigInt == null) { + result *= radix + if (result < limit + digit && maxLenCheck) + return None + result -= digit + } else { + resultBigInt *= radix + if (resultBigInt < limit + digit && maxLenCheck) + return None + resultBigInt -= digit + } + } + } + } + i += 1 + } + if (resultBigInt == null) { + if (result < limit) + None + else { + if (negative) + Some(result) + else + Some(-result) + } + } else { + if (resultBigInt < limit) + None + else { + if (negative) + Some(resultBigInt.toLong) + else + Some(-resultBigInt.toLong) + } + } + } + + private[ast] def toBigInt(value: String): Option[BigInt] = { + var decimalFlag = false + @inline def maxLengthConstant: Int = 10 + var result: Int = 0 // assume that values are initially small when we convert to bigInt + var resultBigInt: BigInt = null + var negative = false + var char: Char = 0 + var i = 0 + var eFlag = false + var trailingZeroes: Int = 0 + var negativeEFlag: Boolean = false + var resultNegativeEFlag: Int = 0 + var digitLength: Int = 0 + if (value.charAt(0) == '-') { + negative = true + i += 1 + } + + while (i < value.length) { + char = value.charAt(i) + if (char == '.') + decimalFlag = true + else if ((char | 0x20) == 'e') { + eFlag = true + val charNext = value.charAt(i + 1) + if (charNext == '-') + negativeEFlag = true + if (negativeEFlag || charNext == '+') { + i += 1 + } + } else { + if (!(eFlag || decimalFlag)) + digitLength += 1 + + val digit = Character.digit(char, radix) + + if (digit == 0) + if (!decimalFlag) + trailingZeroes += 1 + else if (!decimalFlag) + trailingZeroes = 0 + + if (digit != 0 && (decimalFlag || eFlag)) + if (!negativeEFlag) + return None + else { + if (trailingZeroes != 0) { + resultNegativeEFlag *= radix + resultNegativeEFlag += digit + if (trailingZeroes >= resultNegativeEFlag) { + var i2: Int = 0 + while (i2 < resultNegativeEFlag) { + result = result / radix + i2 += 1 + } + } else + return None + } + } + + if (!(digit == 0 && (decimalFlag || eFlag))) { + + if (!negativeEFlag) { + if (digitLength == maxLengthConstant - 1) { + var result2: Long = result + result2 *= radix + + if (!negative && result2 < 0 || negative && result2 > 0) { + resultBigInt = BigInt(result) + } + } + + if (resultBigInt == null) { + result *= radix + result -= digit + } else { + resultBigInt *= radix + resultBigInt -= digit + } + } + } + } + i += 1 + } + + if (resultBigInt == null) { + if (negative) + Some(BigInt(result)) + else + Some(BigInt(-result)) + } else { + if (negative) + Some(resultBigInt) + else + Some(resultBigInt) + } + } + + @inline private[ast] def toDouble(value: String): Double = + value.toDouble + + @inline private[ast] def toFloat(value: String): Float = + value.toFloat + + private[ast] def toBigDecimal(value: String): Option[BigDecimal] = { + try { + Some(BigDecimal(value, MathContext.UNLIMITED)) + } catch { + case _: NumberFormatException | _: ArithmeticException => None + } + } }