All Downloads are FREE. Search and download functionalities are using the official Maven repository.

argonaut.JsonNumber.scala Maven / Gradle / Ivy

There is a newer version: 6.3.10
Show newest version
package argonaut

import java.math.MathContext
import scala.annotation.tailrec
import scala.util.matching.Regex

/**
 * JSON numbers with optimization by cases.
 * Note: Javascript numbers are 64-bit decimals.
 */
sealed abstract class JsonNumber {

  /**
   * Returns this number as a `BigDecimal`.
   */
  def toBigDecimal: BigDecimal

  /**
   * Returns this number as a `Double`, only if this number can be represented as a `Double`.
   */
  lazy val toDouble: Option[Double] = {
    val asDouble = toBigDecimal.toDouble
    if (BigDecimal(asDouble) == toBigDecimal) {
      Some(asDouble)
    } else {
      None
    }
  }

  /**
   * Returns this number as a `Float`, only if this number can be represented as a `Float`.
   */
  lazy val toFloat: Option[Float] = {
    val asFloat = toBigDecimal.toFloat
    if (BigDecimal(asFloat.toDouble) == toBigDecimal) {
      Some(asFloat)
    } else {
      None
    }
  }

  /**
   * Returns this number as a `BigInt`, only if this number is an integer.
   */
  def toBigInt: Option[BigInt] = {
    val n = toBigDecimal
    if (n.isWhole) Some(n.toBigInt)
    else None
  }

  /**
   * Returns this number as a `Long`, only if this number is a valid `Long`.
   */
  def toLong: Option[Long]

  /**
   * Returns this number as a `Int`, only if this number is a valid `Int`.
   */
  def toInt: Option[Int] = toLong.flatMap { n =>
    val m = n.toInt
    if (n == m) Some(m) else None
  }

  /**
   * Returns this number as a `Short`, only if this number is a valid `Short`.
   */
  def toShort: Option[Short] = toLong.flatMap { n =>
    val m = n.toShort
    if (n == m) Some(m) else None
  }

  /**
   * Returns this number as a `Byte`, only if this number is a valid `Byte`.
   */
  def toByte: Option[Byte] = toLong.flatMap { n =>
    val m = n.toByte
    if (n == m) Some(m) else None
  }

  /**
   * Truncates this number to the best `Double` approximation to this number.
   * Anything over `Double.MaxValue` gets rounded to `Double.PositiveInfinity`
   * and anything below `Double.MinValue` gets rounded to
   * `Double.NegativeInfinity`.
   */
  def truncateToDouble: Double = toBigDecimal.toDouble

  /**
   * Truncates this number to the best `Float` approximation to this number.
   * Anything over `Float.MaxValue` gets rounded to `Float.PositiveInfinity`
   * and anything below `Float.MinValue` gets rounded to
   * `Float.NegativeInfinity`.
   */
  def truncateToFloat: Float = toBigDecimal.toFloat

  /**
   * Truncates the number to a BigInt. Truncation means that we round the real
   * number towards 0 to the closest BigInt.
   *
   * Truncation fails for numbers whose decimal representation contains more
   * than 2 ^ 18 digits, since creating `BigInt` values for these numbers is
   * computationally expensive.
   */
  def truncateToBigInt: Option[BigInt] = {
    val asBigDecimal = toBigDecimal
    val digits = asBigDecimal.underlying.unscaledValue.abs.toString.length.toLong - asBigDecimal.scale.toLong

    if (digits <= (1 << 18)) Some(asBigDecimal.toBigInt) else None
  }

  /**
   * Truncates the number to a Long. Truncation means that we round the real
   * number towards 0 to the closest, valid Long. So, if the number is 1e99,
   * then this will return `Long.MaxValue`.
   */
  def truncateToLong: Long = toBigDecimal.toLong

  /**
   * Truncates the number to a Int. Truncation means that we round the real
   * number towards 0 to the closest, valid Int. So, if the number is 1e99,
   * then this will return `Int.MaxValue`.
   */
  def truncateToInt: Int = toBigDecimal.toInt

  /**
   * Truncates the number to a Short. Truncation means that we round the real
   * number towards 0 to the closest, valid Short. So, if the number is 1e99,
   * then this will return `Short.MaxValue`.
   */
  def truncateToShort: Short = {
    val n = truncateToInt
    if (n > Short.MaxValue) Short.MaxValue
    else if (n < Short.MinValue) Short.MinValue
    else n.toShort
  }

  /**
   * Truncates the number to a Byte. Truncation means that we round the real
   * number towards 0 to the closest, valid Byte. So, if the number is 1e99,
   * then this will return `Byte.MaxValue`.
   */
  def truncateToByte: Byte = {
    val n = truncateToInt
    if (n > Byte.MaxValue) Byte.MaxValue
    else if (n < Byte.MinValue) Byte.MinValue
    else n.toByte
  }

  // Force this `JsonNumber` into a `JsonDecimal` by using the underlying
  // numbers toString. Isn't safe is `isReal` is `false.
  private def toJsonDecimal: JsonDecimal = this match {
    case n @ JsonDecimal(_) => n
    case JsonBigDecimal(n) => JsonDecimal(n.toString)
    case JsonLong(n) => JsonDecimal(n.toString)
  }

  override def hashCode: Int = toJsonDecimal.normalized.hashCode

  override def equals(that: Any): Boolean = that match {
    case that: JsonNumber => {
      (this, that) match {
        case (a @ JsonDecimal(_), b) => a.normalized == b.toJsonDecimal.normalized
        case (a, b @ JsonDecimal(_)) => a.toJsonDecimal.normalized == b.normalized
        case (JsonLong(x), JsonLong(y)) => x == y
        case (a, b) => a.toBigDecimal == b.toBigDecimal
      }
    }
    case _ => false
  }

  def asJson: Json = JNumber(this)
}

/**
 * A JsonDecimal represents and valid JSON number as a String. Unfortunately,
 * there is no type in the Scala standard library which can represent all valid
 * JSON decimal numbers, since the exponent may be larger than an `Int`. Such
 * a number can still be round tripped (parser to printer). We lazily parse the
 * string to a `BigDecimal` or a `Double` on demand.
 */
case class JsonDecimal private[argonaut] (value: String) extends JsonNumber {
  lazy val toBigDecimal: BigDecimal = BigDecimal(value, MathContext.UNLIMITED)

  def toLong: Option[Long] = {
    val n = toBigDecimal
    if (n.isValidLong) Some(n.toLong)
    else None
  }

  /**
   * Returns a *normalized* version of this Decimal number. Since BigDecimal
   * cannot represent all valid JSON value exactly, due to the exponent being
   * limited to an `Int`, this method let's us get a normalized number that
   * can be used to compare for equality.
   *
   * The 1st value (BigInt) is the exponent used to scale the 2nd value
   * (BigDecimal) back to the original value represented by this number.  The
   * 2nd BigDecimal will always either be 0 or a number with exactly 1 decimal
   * digit to the right of the decimal point.  If the 2nd value is 0, then the
   * exponent will always be 0 as well.
   */
  def normalized: (BigInt, BigDecimal) = {
    val JsonNumber.JsonNumberRegex(negative, intStr, decStr, expStr) = value

    @tailrec
    def decScale(i: Int): Option[Int] = {
      if (i >= decStr.length) None
      else if (decStr(i) == '0') decScale(i + 1)
      else Some(- i - 1)
    }

    val rescale = {
      if (intStr != "0") Some(intStr.length - 1)
      else if (decStr != null) decScale(0)
      else None
    }

    val unscaledExponent = Option(expStr) match {
      case Some(exp) if exp.startsWith("+") => BigInt(exp.substring(1))
      case Some(exp) => BigInt(exp)
      case None => BigInt(0)
    }
    rescale match {
      case Some(shift) => {
        val unscaledValue =
          if (decStr == null) BigDecimal(intStr, MathContext.UNLIMITED)
          else BigDecimal(s"$intStr.$decStr", MathContext.UNLIMITED)
        val scaledValue = BigDecimal(unscaledValue.bigDecimal.movePointLeft(shift))
        (unscaledExponent + shift, if (negative != null) -scaledValue else scaledValue)
      }

      case None => (BigInt(0), BigDecimal(0))
    }
  }
}

case class JsonBigDecimal(value: BigDecimal) extends JsonNumber {
  def toBigDecimal = value

  def toLong: Option[Long] = {
    if (value.isValidLong) Some(value.toLong)
    else None
  }
}

case class JsonLong(value: Long) extends JsonNumber {
  def toBigDecimal: BigDecimal = BigDecimal(value)
  def toLong: Option[Long] = Some(value)
  override def truncateToLong: Long = value
}

object JsonNumber {
  /**
   * Returns a `JsonNumber` whose value is the valid JSON number in `value`.
   * This value is **not** verified to be a valid JSON string. It is assumed
   * that `value` is a valid JSON number, according to the JSON specification.
   * If the value is invalid then the results are undefined. This is provided
   * for use in places like a Jawn parser facade, which provides its own
   * verification of JSON numbers.
   */
  def unsafeDecimal(value: String): JsonNumber = JsonDecimal(value)

  /**
   * Parses a JSON number from a string. A String is valid if it conforms to
   * the grammar in the JSON specification (RFC 4627 -
   * http://www.ietf.org/rfc/rfc4627.txt), section 2.4. If it is valid, then
   * the number is returned in a `Some`. Otherwise the number is invalid and
   * `None` is returned.
   *
   * @param value a JSON number encoded as a string
   * @return a JSON number if the string is valid
   */
  def fromString(value: String): Option[JsonNumber] = {

    // Span over [0-9]*
    @tailrec
    def digits(index: Int): Int = {
      if (index >= value.length) value.length
      else {
        val char = value(index)
        if (char >= '0' && char <= '9') digits(index + 1)
        else index
      }
    }

    // Verify [0-9]+
    def digits1(index: Int): Int = {
      val end = digits(index)
      if (end == index) -1
      else end
    }

    // Verify 0 | [1-9][0-9]*
    def natural(index: Int): Int = {
      if (index >= value.length) -1
      else {
        val char = value(index)
        if (char == '0') index + 1
        else if (char >= '1' && char <= '9') digits(index + 1)
        else index
      }
    }

    // Verify -?(0 | [1-9][0-9]*)
    def integer(index: Int): Int = {
      if (index >= value.length) -1
      else if (value(index) == '-') natural(index + 1)
      else natural(index)
    }

    // Span .[0-9]+
    def decimal(index: Int): Int = {
      if (index < 0 || index >= value.length) index
      else if (value(index) == '.') digits1(index + 1)
      else index
    }

    // Span e[-+]?[0-9]+
    def exponent(index: Int): Int = {
      if (index < 0 || index >= value.length) index
      else {
        val e = value(index)
        if (e == 'e' || e == 'E') {
          val index0 = index + 1
          if (index0 < value.length) {
            val sign = value(index0)
            if (sign == '+' || sign == '-') digits1(index0 + 1)
            else digits1(index0)
          } else {
            -1
          }
        } else {
          -1
        }
      }
    }

    val intIndex = integer(0)
    val decIndex = decimal(intIndex)
    val expIndex = exponent(decIndex)

    val invalid = {
      (expIndex != value.length) ||
      (intIndex == 0) ||
      (intIndex == -1) ||
      (decIndex == -1)
    }

    // Assuming the number is an integer, does it fit in a Long?
    def isLong: Boolean = {
      val upperBound = if (value(0) == '-') MinLongString else MaxLongString
      (value.length < upperBound.length) ||
        ((value.length == upperBound.length) &&
          value.compareTo(upperBound) <= 0)
    }

    if (invalid) {
      None
    } else if (intIndex == expIndex && isLong) {
      Some(JsonLong(value.toLong))
    } else {
      Some(JsonDecimal(value))
    }
  }

  private val MaxLongString = Long.MaxValue.toString
  private val MinLongString = Long.MinValue.toString

  /**
   * A regular expression that can match a valid JSON number. This has 4 match
   * groups:
   *
   *  1. The optional negative sign.
   *  2. The integer part.
   *  3. The fractional part without the leading period.
   *  4. The exponent part without the leading 'e', but with an optional leading '+' or '-'.
   *
   * The negative sign, fractional part and exponent part are optional matches
   * and may be `null`.
   */
  val JsonNumberRegex: Regex =
    """(-)?((?:[1-9][0-9]*|0))(?:\.([0-9]+))?(?:[eE]([-+]?[0-9]+))?""".r
}

trait EncodeJsonNumber[T] { self =>
  def encodeJsonNumber(value: T): JsonNumber
  def contramap[A](f: A => T): EncodeJsonNumber[A] =
    (value: A) => self.encodeJsonNumber(f(value))
}
trait EncodePossibleJsonNumber[T] { self =>
  def possiblyEncodeJsonNumber(value: T): Option[JsonNumber]
  def contramap[A](f: A => T): EncodePossibleJsonNumber[A] =
    (value: A) => self.possiblyEncodeJsonNumber(f(value))
}

object EncodeJsonNumber {
  implicit val encodeJsonNumberJavaByte: EncodeJsonNumber[java.lang.Byte] =
    (value: java.lang.Byte) => new JsonLong(value.byteValue.toLong)
  implicit val encodeJsonNumberJavaShort: EncodeJsonNumber[java.lang.Short] =
    (value: java.lang.Short) => new JsonLong(value.shortValue.toLong)
  implicit val encodeJsonNumberJavaInteger: EncodeJsonNumber[java.lang.Integer] =
    (value: java.lang.Integer) => new JsonLong(value.intValue.toLong)
  implicit val encodeJsonNumberJavaLong: EncodeJsonNumber[java.lang.Long] =
    (value: java.lang.Long) => new JsonLong(value.longValue)
  implicit val encodeJsonNumberJavaFloat: EncodePossibleJsonNumber[java.lang.Float] =
    (value: java.lang.Float) => encodeJsonNumberFloat.possiblyEncodeJsonNumber(value.floatValue)
  implicit val encodeJsonNumberJavaDouble: EncodePossibleJsonNumber[java.lang.Double] =
    (value: java.lang.Double) => encodeJsonNumberDouble.possiblyEncodeJsonNumber(value.doubleValue)
  implicit val encodeJsonNumberByte: EncodeJsonNumber[Byte] =
    (value: Byte) => new JsonLong(value.toLong)
  implicit val encodeJsonNumberShort: EncodeJsonNumber[Short] =
    (value: Short) => new JsonLong(value.toLong)
  implicit val encodeJsonNumberInt: EncodeJsonNumber[Int] =
    (value: Int) => new JsonLong(value.toLong)
  implicit val encodeJsonNumberLong: EncodeJsonNumber[Long] =
    (value: Long) => new JsonLong(value)
  implicit val encodeJsonNumberFloat: EncodePossibleJsonNumber[Float] = (value: Float) => {
    if (value.isInfinity || java.lang.Float.isNaN(value)) {
      None
    } else {
      Some(new JsonDecimal(value.toString))
    }
  }
  implicit val encodeJsonNumberDouble: EncodePossibleJsonNumber[Double] = (value: Double) => {
    if (value.isInfinity || java.lang.Double.isNaN(value)) {
      None
    } else {
      Some(new JsonDecimal(value.toString))
    }
  }
  implicit val encodeJsonNumberBigInt: EncodeJsonNumber[BigInt] =
    (value: BigInt) => new JsonBigDecimal(BigDecimal(value, java.math.MathContext.UNLIMITED))
  implicit val encodeJsonNumberBigDecimal: EncodeJsonNumber[BigDecimal] =
    (value: BigDecimal) => new JsonBigDecimal(value)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy