Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Basic implementation of JNumber conversions #42

Merged
merged 13 commits into from
Dec 11, 2017
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions benchmark/jvm/src/test/scala/benchmark/Constants.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package benchmark

object Constants {
@inline def innerLoopConstant: Int = 10000
}
3 changes: 2 additions & 1 deletion benchmark/jvm/src/test/scala/benchmark/Generators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
51 changes: 50 additions & 1 deletion benchmark/jvm/src/test/scala/benchmark/JNumber.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions benchmark/jvm/src/test/scala/benchmark/unsafe/JNumber.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package benchmark.unsafe

import benchmark.Constants
import org.scalameter.Bench

object JNumber extends Bench.ForkedTime {
Expand All @@ -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
}
}
}
}
}
20 changes: 12 additions & 8 deletions js/src/main/scala-2.10/scalajson.ast/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
44 changes: 26 additions & 18 deletions js/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.##
}
})
Expand Down Expand Up @@ -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.##
}
})
Expand Down
19 changes: 12 additions & 7 deletions js/src/main/scala/scalajson/ast/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
16 changes: 12 additions & 4 deletions js/src/main/scala/scalajson/ast/unsafe/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions jvm/src/main/scala-2.10/scalajson.ast/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions jvm/src/main/scala-2.10/scalajson.ast/unsafe/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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: Double = scalajson.ast.toFloat(value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Fixed


def toBigDecimal: Option[BigDecimal] = scalajson.ast.toBigDecimal(value)
}

/** Represents a JSON Boolean value, which can either be a
Expand Down
12 changes: 12 additions & 0 deletions jvm/src/main/scala/scalajson/ast/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading