Skip to content

Commit

Permalink
Added JsonLong and JsonDouble cases for JsonNumber #116
Browse files Browse the repository at this point in the history
  • Loading branch information
runarorama committed Sep 24, 2014
1 parent fe3c6bf commit b405f23
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 45 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ target
/project/boot
*.swp
repl-port
*~
tags
6 changes: 3 additions & 3 deletions src/main/scala/argonaut/DecodeJson.scala
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,13 @@ trait DecodeJsons extends GeneratedDecodeJsons with internal.MacrosCompat {
optionDecoder(_.number map (_.toFloat), "java.lang.Float")

implicit def JIntegerDecodeJson: DecodeJson[java.lang.Integer] =
optionDecoder(_.string flatMap (s => tryTo(s.toInt)), "java.lang.Integer")
optionDecoder(_.number flatMap (s => tryTo(s.toInt)), "java.lang.Integer")

implicit def JLongDecodeJson: DecodeJson[java.lang.Long] =
optionDecoder(_.string flatMap (s => tryTo(s.toLong)), "java.lang.Long")
optionDecoder(_.number flatMap (s => tryTo(s.toLong)), "java.lang.Long")

implicit def JShortDecodeJson: DecodeJson[java.lang.Short] =
optionDecoder(_.string flatMap (s => tryTo(s.toShort)), "java.lang.Short")
optionDecoder(_.number flatMap (s => tryTo(s.toShort)), "java.lang.Short")

implicit def JBooleanDecodeJson: DecodeJson[java.lang.Boolean] =
optionDecoder(_.bool map (q => q), "java.lang.Boolean")
Expand Down
20 changes: 10 additions & 10 deletions src/main/scala/argonaut/EncodeJson.scala
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,19 @@ trait EncodeJsons extends GeneratedEncodeJsons with internal.MacrosCompat {
EncodeJson(jString)

implicit val DoubleEncodeJson: EncodeJson[Double] =
EncodeJson(jNumberOrNull)
EncodeJson(a => JsonDouble(a).asJsonOrNull)

implicit val FloatEncodeJson: EncodeJson[Float] =
EncodeJson(a => jNumberOrNull(a))
EncodeJson(a => JsonDouble(a.toDouble).asJsonOrNull)

implicit val IntEncodeJson: EncodeJson[Int] =
EncodeJson(a => jNumberOrNull(a.toDouble))
EncodeJson(a => JsonLong(a.toLong).asJsonOrNull)

implicit val LongEncodeJson: EncodeJson[Long] =
EncodeJson(a => jString(a.toString))
EncodeJson(a => JsonLong(a).asJsonOrNull)

implicit val ShortEncodeJson: EncodeJson[Short] =
EncodeJson(a => jString(a.toString))
EncodeJson(a => JsonLong(a.toLong).asJsonOrNull)

implicit val BooleanEncodeJson: EncodeJson[Boolean] =
EncodeJson(jBool)
Expand All @@ -122,19 +122,19 @@ trait EncodeJsons extends GeneratedEncodeJsons with internal.MacrosCompat {
EncodeJson(a => jString(a.toString))

implicit val JDoubleEncodeJson: EncodeJson[java.lang.Double] =
EncodeJson(a => jNumberOrNull(a.doubleValue))
EncodeJson(a => JsonDouble(a.doubleValue).asJsonOrNull)

implicit val JFloatEncodeJson: EncodeJson[java.lang.Float] =
EncodeJson(a => jNumberOrNull(a.floatValue.toDouble))
EncodeJson(a => JsonDouble(a.floatValue.toDouble).asJsonOrNull)

implicit val JIntegerEncodeJson: EncodeJson[java.lang.Integer] =
EncodeJson(a => jString(a.toString))
EncodeJson(a => JsonLong(a.intValue.toLong).asJsonOrNull)

implicit val JLongEncodeJson: EncodeJson[java.lang.Long] =
EncodeJson(a => jString(a.toString))
EncodeJson(a => JsonLong(a.longValue).asJsonOrNull)

implicit val JShortEncodeJson: EncodeJson[java.lang.Short] =
EncodeJson(a => jString(a.toString))
EncodeJson(a => JsonLong(a.shortValue.toLong).asJsonOrNull)

implicit val JBooleanEncodeJson: EncodeJson[java.lang.Boolean] =
EncodeJson(a => jBool(a.booleanValue))
Expand Down
87 changes: 74 additions & 13 deletions src/main/scala/argonaut/Json.scala
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ sealed trait Json {
* Returns this JSON number object or the value `0` if it is not a number.
*/
def numberOrZero: JsonNumber =
numberOr(0D)
numberOr(JsonLong(0L))

/**
* Returns the string of this JSON value, or an empty string if this JSON value is not a string.
Expand Down Expand Up @@ -497,7 +497,6 @@ trait Jsons {
type JsonField = String
type JsonAssoc = (JsonField, Json)
type JsonObjectMap = scalaz.InsertionMap[JsonField, Json]
type JsonNumber = Double

import PLens._, StoreT._

Expand All @@ -516,13 +515,27 @@ trait Jsons {
* Note: It is an invalid Prism for NaN, +Infinity and -Infinity as they are not valid json.
*/
def jDoublePrism: SimplePrism[Json, Double] =
SimplePrism[Json, Double](d => JNumber(d), _.fold(None, _ => None, n => Some(n), _ => None, _ => None, _ => None))
SimplePrism[Json, Double](
d => JNumber(JsonDouble(d)),
_.fold(None,
_ => None,
n => Some(n.toDouble),
_ => None,
_ => None,
_ => None))

/**
* A Prism for JSON integer values.
*/
def jIntPrism: SimplePrism[Json, Int] =
SimplePrism[Json, Int](i => JNumber(i.toDouble), _.fold(None, _ => None, n => safeCast[Double, Int].getOption(n), _ => None, _ => None, _ => None))
SimplePrism[Json, Int](
i => JNumber(JsonLong(i.toLong)),
_.fold(None,
_ => None,
n => n.safeInt,
_ => None,
_ => None,
_ => None))

/**
* A Prism for JSON string values.
Expand Down Expand Up @@ -626,18 +639,15 @@ trait Jsons {
*
* Note: NaN, +Infinity and -Infinity are not valid json.
*/
val jNumber: JsonNumber => Option[Json] =
number =>
(!number.isNaN && !number.isInfinity).option(JNumber(number))
def jNumber(n: Int): Option[Json] = JsonLong(n).asJson

/**
* Construct a JSON value that is a number. Transforming
* NaN, +Infinity and -Infinity to jNull. This matches
* the behaviour of most browsers, but is a lossy operation
* as you can no longer distinguish between NaN and Infinity.
*/
val jNumberOrNull: JsonNumber => Json =
number => jNumber(number).getOrElse(jNull)
def jNumberOrNull(n: Int): Json = JsonLong(n).asJsonOrNull

/**
* Construct a JSON value that is a number. Transforming
Expand All @@ -648,8 +658,59 @@ trait Jsons {
* interoperability is unlikely without custom handling of
* these values. See also `jNumber` and `jNumberOrNull`.
*/
val jNumberOrString: JsonNumber => Json =
number => jNumber(number).getOrElse(jString(number.toString))
def jNumberOrString(n: Int): Json = JsonLong(n).asJsonOrString

/**
* Construct a JSON value that is a number.
*
* Note: NaN, +Infinity and -Infinity are not valid json.
*/
def jNumber(n: Long): Option[Json] = JsonLong(n).asJson

/**
* Construct a JSON value that is a number. Transforming
* NaN, +Infinity and -Infinity to jNull. This matches
* the behaviour of most browsers, but is a lossy operation
* as you can no longer distinguish between NaN and Infinity.
*/
def jNumberOrNull(n: Long): Json = JsonLong(n).asJsonOrNull

/**
* Construct a JSON value that is a number. Transforming
* NaN, +Infinity and -Infinity to their string implementations.
*
* This is an argonaut specific transformation that allows all
* doubles to be encoded without losing information, but aware
* interoperability is unlikely without custom handling of
* these values. See also `jNumber` and `jNumberOrNull`.
*/
def jNumberOrString(n: Long): Json = JsonLong(n).asJsonOrString

/**
* Construct a JSON value that is a number.
*
* Note: NaN, +Infinity and -Infinity are not valid json.
*/
def jNumber(n: Double): Option[Json] = JsonDouble(n).asJson

/**
* Construct a JSON value that is a number. Transforming
* NaN, +Infinity and -Infinity to jNull. This matches
* the behaviour of most browsers, but is a lossy operation
* as you can no longer distinguish between NaN and Infinity.
*/
def jNumberOrNull(n: Double): Json = JsonDouble(n).asJsonOrNull

/**
* Construct a JSON value that is a number. Transforming
* NaN, +Infinity and -Infinity to their string implementations.
*
* This is an argonaut specific transformation that allows all
* doubles to be encoded without losing information, but aware
* interoperability is unlikely without custom handling of
* these values. See also `jNumber` and `jNumberOrNull`.
*/
def jNumberOrString(n: Double): Json = JsonDouble(n).asJsonOrString

/**
* Construct a JSON value that is a string.
Expand Down Expand Up @@ -685,7 +746,7 @@ trait Jsons {
* A JSON value that is a zero number.
*/
val jZero: Json =
JNumber(0D)
JNumber(JsonLong(0L))

/**
* A JSON value that is an empty string.
Expand Down Expand Up @@ -749,7 +810,7 @@ trait Jsons {
a1 match {
case JNull => a2.isNull
case JBool(b) => a2.bool exists (_ == b)
case JNumber(n) => a2.number exists (_ == n)
case JNumber(n) => a2.number exists (_ === n)
case JString(s) => a2.string exists (_ == s)
case JArray(a) => a2.array exists (_ === a)
case JObject(o) => a2.obj exists (_ === o)
Expand Down
87 changes: 87 additions & 0 deletions src/main/scala/argonaut/JsonNumber.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package argonaut

import scalaz.Equal
import scalaz.Scalaz._
import monocle.function._
import monocle.std._

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

def toDouble: Double
def toFloat: Float
def toInt: Int
def toLong: Long
def toShort: Short

/** Safely coerce to an `Int` if this number fits in an `Int`, otherwise `None` */
def safeInt: Option[Int] = safeCast[Double, Int].getOption(toDouble)

/** Safely coerce to a `Long` if this number fits in a `Long`, otherwise `None` */
def safeLong: Option[Long] = {
val n = toDouble
(n.floor == n) option toLong
}

def isNaN: Boolean = false
def isInfinity: Boolean = false

/**
* Construct a JSON value that is a number.
*
* Note: NaN, +Infinity and -Infinity are not valid json.
*/
def asJson: Option[Json] =
(!isNaN && !isInfinity).option(JNumber(this))

/**
* Construct a JSON value that is a number. Transforming
* NaN, +Infinity and -Infinity to jNull. This matches
* the behaviour of most browsers, but is a lossy operation
* as you can no longer distinguish between NaN and Infinity.
*/
def asJsonOrNull: Json =
asJson.getOrElse(jNull)

/**
* Construct a JSON value that is a number. Transforming
* NaN, +Infinity and -Infinity to their string implementations.
*
* This is an argonaut specific transformation that allows all
* doubles to be encoded without losing information, but aware
* interoperability is unlikely without custom handling of
* these values. See also `jNumber` and `jNumberOrNull`.
*/
def asJsonOrString: Json =
asJson.getOrElse(jString(toString))
}

case class JsonLong(value: Long) extends JsonNumber {
def toDouble = value.toDouble
def toFloat = value.toFloat
def toInt = value.toInt
def toLong = value
def toShort = value.toShort
}
case class JsonDouble(value: Double) extends JsonNumber {
def toDouble = value
def toFloat = value.toFloat
def toInt = value.toInt
def toLong = value.toLong
def toShort = value.toShort
override def isNaN = value.isNaN
override def isInfinity = value.isInfinity
}

object JsonNumber {
implicit val JsonNumberEqual: Equal[JsonNumber] = new Equal[JsonNumber] {
def equal(a: JsonNumber, b: JsonNumber) = a match {
case JsonLong(n) => n == b.toLong
case JsonDouble(n) => n == b.toDouble
}
}
}
8 changes: 4 additions & 4 deletions src/main/scala/argonaut/JsonParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ object JsonParser {
if (numberEndIndex == position) unexpectedContent(stream, position)
else {
val numberAsString = stream.substring(position, numberEndIndex)
numberAsString
.parseDouble
.fold(nfe => "Value [%s] cannot be parsed into a number.".format(numberAsString).left,
doubleValue => \/-((numberEndIndex, jNumberOrNull(doubleValue))))
(numberAsString.parseLong.map(JsonLong(_)) orElse
numberAsString.parseDouble.map(JsonDouble(_))).fold(
nfe => "Value [%s] cannot be parsed into a number.".format(numberAsString).left,
jn => \/-((numberEndIndex, jn.asJsonOrNull)))
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/main/scala/argonaut/PrettyParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ sealed trait PrettyParams {

private[this] def vectorMemo() = {
var vector: Vector[String] = Vector.empty

val memoFunction: (Int => String) => Int => String = f => k => {
val localVector = vector
val adjustedK = if (k < 0) 0 else k
Expand Down Expand Up @@ -166,7 +166,10 @@ sealed trait PrettyParams {
k.fold[StringBuilder](
builder.append(nullText)
, bool => builder.append(if (bool) trueText else falseText)
, n => builder.append(if (n == n.floor) BigDecimal(n).toBigInt.toString else n.toString)
, n => n match {
case JsonLong(x) => builder append x.toString
case JsonDouble(x) => builder append x.toString
}
, s => encloseJsonString(builder, s)
, e => {
rbracket(e.foldLeft((true, lbracket(builder))){case ((firstElement, builder), subElement) =>
Expand Down
5 changes: 3 additions & 2 deletions src/test/scala/argonaut/CodecNumberSpecification.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ object CodecNumberSpecification extends Specification with ScalaCheck {
Codec Numbers
double that is not NaN or infinity encodes to number $double
int always encodes to number $intToNumber
long always encodes to string $longToString
long always encodes to number $longToNumber
"""

def double = prop { (xs: List[Double]) => xs.filter(x => !x.isNaN && !x.isInfinity).asJson.array.forall(_.forall(_.isNumber)) }

def intToNumber = prop { (xs: List[Int]) => xs.asJson.array.forall(_.forall(_.isNumber)) }

def longToString = prop { (xs: List[Long]) => xs.asJson.array.forall(_.forall(_.isString)) }
def longToNumber = prop { (xs: List[Long]) => xs.asJson.array.forall(_.forall(_.isNumber)) }

}
11 changes: 10 additions & 1 deletion src/test/scala/argonaut/Data.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import scala.util.Random.shuffle
object Data {
val maxJsonStructureDepth = 3

val jsonNumberGenerator: Gen[JNumber] = arbitrary[Double].map(number => JNumber(number))
val jsonNumberRepGenerator: Gen[JsonNumber] = Gen.oneOf(
arbitrary[Double].map(JsonDouble(_)),
arbitrary[Long].map(JsonLong(_))
)

val jsonNumberGenerator: Gen[JNumber] =
jsonNumberRepGenerator.map(number => JNumber(number))

def isValidJSONCharacter(char: Char): Boolean = !char.isControl && char != '\\' && char != '\"'

Expand Down Expand Up @@ -76,6 +82,9 @@ object Data {

implicit def ArbitraryJNumber: Arbitrary[JNumber] = Arbitrary(jsonNumberGenerator)

implicit def ArbitraryJsonNumber: Arbitrary[JsonNumber] =
Arbitrary(jsonNumberRepGenerator)

implicit def ArbitraryJArray: Arbitrary[JArray] = Arbitrary(jsonArrayGenerator())

implicit def ArbitraryJObject: Arbitrary[JObject] = Arbitrary(jsonObjectGenerator())
Expand Down
2 changes: 1 addition & 1 deletion src/test/scala/argonaut/JsonParserSpecification.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object JsonParserSpecification extends Specification with DataTables with ScalaC
val whitespaceGen: Gen[String] = listOf(Gen.oneOf(' ', '\n', '\r', '\t')).map(_.mkString)

val whitespaceObjectGen: Gen[String] = whitespaceGen.map(whitespace => """#{#"field1"#:#12#,#"field2"#:#"test"#}#""".replace("#", whitespace))
val whitespaceObject: Json = ("field1" := 12.0d) ->: ("field2" := "test") ->: jEmptyObject
val whitespaceObject: Json = ("field1" := 12) ->: ("field2" := "test") ->: jEmptyObject

val whitespaceArrayGen: Gen[String] = whitespaceGen.map(whitespace => """#[#"value1"#,#12#]#""".replace("#", whitespace))
val whitespaceArray: Json = jArray(jString("value1") :: jNumberOrNull(12) :: Nil)
Expand Down
Loading

0 comments on commit b405f23

Please sign in to comment.