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/js/src/main/scala-2.10/scalajson.ast/JValue.scala b/js/src/main/scala-2.10/scalajson.ast/JValue.scala index 21ffacf..4ba776e 100644 --- a/js/src/main/scala-2.10/scalajson.ast/JValue.scala +++ b/js/src/main/scala-2.10/scalajson.ast/JValue.scala @@ -145,7 +145,9 @@ final class JNumber private[ast] (val value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } 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 cfa115c..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,14 +87,14 @@ 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) } 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) @@ -103,7 +103,9 @@ final case class JNumber(value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } @@ -235,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.## } }) @@ -307,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 eda7876..6f46c83 100644 --- a/js/src/main/scala/scalajson/ast/JValue.scala +++ b/js/src/main/scala/scalajson/ast/JValue.scala @@ -128,7 +128,9 @@ final case class JNumber private[ast] (value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } diff --git a/js/src/main/scala/scalajson/ast/unsafe/JValue.scala b/js/src/main/scala/scalajson/ast/unsafe/JValue.scala index 71150ce..9d5eed8 100644 --- a/js/src/main/scala/scalajson/ast/unsafe/JValue.scala +++ b/js/src/main/scala/scalajson/ast/unsafe/JValue.scala @@ -101,7 +101,9 @@ final case class JNumber(value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } 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 39b3c69..99cbfc1 100644 --- a/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala +++ b/jvm/src/main/scala-2.10/scalajson.ast/JValue.scala @@ -132,7 +132,9 @@ final class JNumber private[ast] (val value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } 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 ba87956..e74aacb 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 @@ -83,7 +83,9 @@ final case class JNumber(value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Double = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } diff --git a/jvm/src/main/scala/scalajson/ast/JValue.scala b/jvm/src/main/scala/scalajson/ast/JValue.scala index 461d338..011eb72 100644 --- a/jvm/src/main/scala/scalajson/ast/JValue.scala +++ b/jvm/src/main/scala/scalajson/ast/JValue.scala @@ -113,7 +113,9 @@ final case class JNumber private[ast] (value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } diff --git a/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala b/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala index ae359f8..3a7c660 100644 --- a/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala +++ b/jvm/src/main/scala/scalajson/ast/unsafe/JValue.scala @@ -83,7 +83,9 @@ final case class JNumber(value: String) extends JValue { def toLong: Option[Long] = scalajson.ast.toLong(value) - def toDouble: Option[Double] = scalajson.ast.toDouble(value) + def toDouble: Double = scalajson.ast.toDouble(value) + + def toFloat: Float = scalajson.ast.toFloat(value) def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value) } 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 7ddb889..c837b2d 100644 --- a/shared/src/main/scala-2.10/scalajson.ast/package.scala +++ b/shared/src/main/scala-2.10/scalajson.ast/package.scala @@ -698,25 +698,18 @@ package object ast { } } - private[ast] def toDouble(value: String): Option[Double] = { - try { - val asDouble = value.toDouble - if (BigDecimal(value, MathContext.UNLIMITED) == BigDecimal( - asDouble, - MathContext.UNLIMITED)) - Some(asDouble) - else - None - } catch { - case _: java.lang.NumberFormatException => None - } - } + @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 _: java.lang.NumberFormatException => None + 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 7ddb889..bd335c2 100644 --- a/shared/src/main/scala/scalajson/ast/package.scala +++ b/shared/src/main/scala/scalajson/ast/package.scala @@ -698,25 +698,17 @@ package object ast { } } - private[ast] def toDouble(value: String): Option[Double] = { - try { - val asDouble = value.toDouble - if (BigDecimal(value, MathContext.UNLIMITED) == BigDecimal( - asDouble, - MathContext.UNLIMITED)) - Some(asDouble) - else - None - } catch { - case _: java.lang.NumberFormatException => None - } - } + @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 _: java.lang.NumberFormatException => None + case _: NumberFormatException | _: ArithmeticException => None } } }