diff --git a/.gitignore b/.gitignore index a952d0c4..60acc430 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ target /project/boot *.swp repl-port +*~ +tags diff --git a/src/main/scala/argonaut/DecodeJson.scala b/src/main/scala/argonaut/DecodeJson.scala index cb3cea03..a15b2e8c 100644 --- a/src/main/scala/argonaut/DecodeJson.scala +++ b/src/main/scala/argonaut/DecodeJson.scala @@ -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") diff --git a/src/main/scala/argonaut/EncodeJson.scala b/src/main/scala/argonaut/EncodeJson.scala index 3a73aa87..f5bd3f1a 100644 --- a/src/main/scala/argonaut/EncodeJson.scala +++ b/src/main/scala/argonaut/EncodeJson.scala @@ -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) @@ -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)) diff --git a/src/main/scala/argonaut/Json.scala b/src/main/scala/argonaut/Json.scala index 55c486ff..e6347a34 100644 --- a/src/main/scala/argonaut/Json.scala +++ b/src/main/scala/argonaut/Json.scala @@ -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. @@ -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._ @@ -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. @@ -626,9 +639,7 @@ 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 @@ -636,8 +647,7 @@ trait Jsons { * 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 @@ -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. @@ -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. @@ -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) diff --git a/src/main/scala/argonaut/JsonNumber.scala b/src/main/scala/argonaut/JsonNumber.scala new file mode 100644 index 00000000..2d900cda --- /dev/null +++ b/src/main/scala/argonaut/JsonNumber.scala @@ -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 + } + } +} diff --git a/src/main/scala/argonaut/JsonParser.scala b/src/main/scala/argonaut/JsonParser.scala index 6f93aaa0..2ba5d15f 100644 --- a/src/main/scala/argonaut/JsonParser.scala +++ b/src/main/scala/argonaut/JsonParser.scala @@ -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))) } } } diff --git a/src/main/scala/argonaut/PrettyParams.scala b/src/main/scala/argonaut/PrettyParams.scala index 3fdb7f83..b35ff0bd 100644 --- a/src/main/scala/argonaut/PrettyParams.scala +++ b/src/main/scala/argonaut/PrettyParams.scala @@ -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 @@ -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) => diff --git a/src/test/scala/argonaut/CodecNumberSpecification.scala b/src/test/scala/argonaut/CodecNumberSpecification.scala index 2e5b7097..0e868f67 100644 --- a/src/test/scala/argonaut/CodecNumberSpecification.scala +++ b/src/test/scala/argonaut/CodecNumberSpecification.scala @@ -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)) } + } diff --git a/src/test/scala/argonaut/Data.scala b/src/test/scala/argonaut/Data.scala index 3be0fd15..ce055081 100644 --- a/src/test/scala/argonaut/Data.scala +++ b/src/test/scala/argonaut/Data.scala @@ -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 != '\"' @@ -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()) diff --git a/src/test/scala/argonaut/JsonParserSpecification.scala b/src/test/scala/argonaut/JsonParserSpecification.scala index 71ac6a4a..2b9ffc17 100644 --- a/src/test/scala/argonaut/JsonParserSpecification.scala +++ b/src/test/scala/argonaut/JsonParserSpecification.scala @@ -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) diff --git a/src/test/scala/argonaut/JsonSpecification.scala b/src/test/scala/argonaut/JsonSpecification.scala index dd0bd3f6..a67efa5e 100644 --- a/src/test/scala/argonaut/JsonSpecification.scala +++ b/src/test/scala/argonaut/JsonSpecification.scala @@ -40,8 +40,12 @@ object JsonSpecification extends Specification with ScalaCheck { def sameValue = prop((j: Json) => j === j) def modString = prop((j: JString) => j.withString(_ + "test") /== j) - - def modNumber = prop((j: JNumber) => j.withNumber(number => if (number === 0.0d) number + 1 else number * 2) /== j) + + def modNumber = prop((j: JNumber) => j.withNumber { number => + JsonLong(number.safeInt.map(n => + if (n === 0) (n + 1) else (n * 2) + ).getOrElse(0).toLong) + } /== j) def modArray = prop((j: JArray) => j.withArray(jEmptyArray :: _) /== j) @@ -52,7 +56,7 @@ object JsonSpecification extends Specification with ScalaCheck { def notComposeNot = prop((j: Json) => j.not.not === j) def noEffect = prop((j: Json) => (j.not === j) !== j.isBool) - + def effectNotIsBool = prop((j: Json) => (j.not /== j) === j.isBool) def effectWithNumber = prop((j: Json, k: JsonNumber => JsonNumber) => ((j withNumber k) === j) || j.isNumber) @@ -61,13 +65,13 @@ object JsonSpecification extends Specification with ScalaCheck { def effectWithArray = prop((j: Json, k: List[Json] => List[Json]) => ((j withArray k) === j) || j.isArray) - def effectWithObject = prop((j: Json, k: JsonObject => JsonObject) => ((j withObject k) === j) || j.isObject) + def effectWithObject = prop((j: Json, k: JsonObject => JsonObject) => ((j withObject k) === j) || j.isObject) def arrayPrepend = prop((j: Json, e: Json) => !j.isArray || (e -->>: j).array.map(_.head) === e.some) def isBool = prop((b: Boolean) => jBool(b).isBool) - def isNumber = prop((n: JsonNumber) => !n.isNaN && !n.isInfinity ==> jNumberOrNull(n).isNumber) + def isNumber = prop((n: JsonNumber) => !n.isNaN && !n.isInfinity ==> n.asJsonOrNull.isNumber) def isString = prop((s: String) => jString(s).isString) diff --git a/src/test/scala/argonaut/KnownResults.scala b/src/test/scala/argonaut/KnownResults.scala index cceca298..a7768771 100644 --- a/src/test/scala/argonaut/KnownResults.scala +++ b/src/test/scala/argonaut/KnownResults.scala @@ -37,7 +37,7 @@ object KnownResults extends DataTables { "1E999" ! jNumberOrNull("1E999".toDouble) | "1E+999" ! jNumberOrNull("1E+999".toDouble) | "1E-999" ! jNumberOrNull("1E-999".toDouble) | - "158699798998941697" ! jNumberOrNull(158699798998941697D) + "158699798998941697" ! jNumberOrNull(158699798998941697L) def parseFailures = "JSON" | "parse result" | diff --git a/src/test/scala/argonaut/PrettyParamsSpecification.scala b/src/test/scala/argonaut/PrettyParamsSpecification.scala index 79705331..e101025b 100644 --- a/src/test/scala/argonaut/PrettyParamsSpecification.scala +++ b/src/test/scala/argonaut/PrettyParamsSpecification.scala @@ -173,7 +173,7 @@ object PrettyParamsSpecification extends Specification with ScalaCheck { } ^ end val numbers: Fragments = "number printing" ^ "whole number pretty print" ! prop{(n: Long) => - jNumberOrNull(n).nospaces === "%.0f".format(n.toDouble) + jNumberOrNull(n).nospaces === n.toString } ^ "fractional number pretty print" ! forAll(arbitrary[(Double, Double)].filter{case (first, second) => second != 0}.map(pair => pair._1 / pair._2).filter(d => d != d.floor)){d => jNumberOrNull(d).nospaces === d.toString diff --git a/src/test/scala/argonaut/StringWrapSpecification.scala b/src/test/scala/argonaut/StringWrapSpecification.scala index 01726383..c55ae2c3 100644 --- a/src/test/scala/argonaut/StringWrapSpecification.scala +++ b/src/test/scala/argonaut/StringWrapSpecification.scala @@ -24,7 +24,7 @@ object StringWrapSpecification extends Specification with ScalaCheck { parse Optional encode ${ (("optional" :?= (None: Option[String])) ->?: jEmptyObject) must_== jEmptyObject - } + } Optional encode alias ${ prop { (o: Option[Int]) => (("optional" :?= o) ->?: jEmptyObject) must_== (("optional" :?= o) ->?: jEmptyObject) @@ -85,7 +85,7 @@ object StringWrapSpecification extends Specification with ScalaCheck { json.decodeWith[Option[Person], Person](_ => None, _ => None, (_, _) => Person("Test", 5).some) === Person("Test", 5).some } } - + decodeOr[A, X: DecodeJson](X => A, => A): A returns the decoded and transformed Json for valid JSON ${ forAllNoShrink(alphaStr, arbitrary[Int]) { (name: String, age: Int) =>