I'm attemptin to include the Double
value of 0.81
in some JSON generated by NSJSONSerialization
. The code is as follows:
let jsonInput = [ "value": 0.81 ]
let data = try NSJSONSerialization.dataWithJSONObject(jsonInput, options: NSJSONWritingOptions.PrettyPrinted)
let json = NSString(data: data, encoding: NSUTF8StringEncoding)!
print( json )
The output is:
{
"value" : 0.8100000000000001
}
But what I'd like to see is:
{
"value" : 0.81
}
How can I make NSJSONSerialization
do this?
One further thing that is confusing me here is Swift's handling of the 64bit Double
. As in the playground I can also do this:
let eightOne:Double = 0.81
"\(eightOne)"
print( eightOne )
And the output is then as desired with:
0.81
Even though in the playground it shows eightOne
as 0.8100000000000001
as far as internal representation goes. However here when it converts to string it chops off the rest.
I'm surely this is solved, as you'd need it sorted for any kind of financial handling (eg. in Java we know we only use BigDecimals
when it comes to financial values).
Please help. :)
NOTE: The focus here is on serialization to JSON. Not just a simple call off to NSString( format: "%\(0.2)f", 0.81)
.
For precise base-10 arithmetic (up to 38 significant digits)
you can use NSDecimalNumber
:
let jsonInput = [ "value": NSDecimalNumber(string: "0.81") ]
or
let val = NSDecimalNumber(integer: 81).decimalNumberByDividingBy(NSDecimalNumber(integer: 100))
let jsonInput = [ "value": val ]
Then
let data = try NSJSONSerialization.dataWithJSONObject(jsonInput, options: NSJSONWritingOptions.PrettyPrinted)
let json = NSString(data: data, encoding: NSUTF8StringEncoding)!
print( json )
produces the output
{
"value" : 0.81
}
You'll need to convert your Double
to a Decimal
to keep its expected string representation when serializing.
One way to avoid a precision of 16 digits may be to round with a scale of 15:
(0.81 as NSDecimalNumber).rounding(accordingToBehavior: NSDecimalNumberHandler(roundingMode: .plain, scale: 15, raiseOnExactness: false, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true)) as Decimal
To automatically and recursively do it for all Double
values in your JSON object, being it a Dictionary or an Array, you can use:
import Foundation
/// https://stackoverflow.com/q/35053577/1033581
extension JSONSerialization {
/// Produce Double values as Decimal values.
open class func decimalData(withJSONObject obj: Any, options opt: JSONSerialization.WritingOptions = []) throws -> Data {
return try data(withJSONObject: decimalObject(obj), options: opt)
}
/// Write Double values as Decimal values.
open class func writeDecimalJSONObject(_ obj: Any, to stream: OutputStream, options opt: JSONSerialization.WritingOptions = [], error: NSErrorPointer) -> Int {
return writeJSONObject(decimalObject(obj), to: stream, options: opt, error: error)
}
fileprivate static let roundingBehavior = NSDecimalNumberHandler(roundingMode: .plain, scale: 15, raiseOnExactness: false, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true)
fileprivate static func decimalObject(_ anObject: Any) -> Any {
let value: Any
if let n = anObject as? [String: Any] {
// subclassing children
let dic = DecimalDictionary()
n.forEach { dic.setObject($1, forKey: $0) }
value = dic
} else if let n = anObject as? [Any] {
// subclassing children
let arr = DecimalArray()
n.forEach { arr.add($0) }
value = arr
} else if let n = anObject as? NSNumber, CFNumberGetType(n) == .float64Type {
// converting precision for correct decimal output
value = NSDecimalNumber(value: anObject as! Double).rounding(accordingToBehavior: roundingBehavior)
} else {
value = anObject
}
return value
}
}
private class DecimalDictionary: NSDictionary {
let _dictionary: NSMutableDictionary = [:]
override var count: Int {
return _dictionary.count
}
override func keyEnumerator() -> NSEnumerator {
return _dictionary.keyEnumerator()
}
override func object(forKey aKey: Any) -> Any? {
return _dictionary.object(forKey: aKey)
}
func setObject(_ anObject: Any, forKey aKey: String) {
let value = JSONSerialization.decimalObject(anObject)
_dictionary.setObject(value, forKey: aKey as NSString)
}
}
private class DecimalArray: NSArray {
let _array: NSMutableArray = []
override var count: Int {
return _array.count
}
override func object(at index: Int) -> Any {
return _array.object(at: index)
}
func add(_ anObject: Any) {
let value = JSONSerialization.decimalObject(anObject)
_array.add(value)
}
}
JSONSerialization.decimalData(withJSONObject: [ "value": 0.81 ], options: [])
If you need fine tuning of decimal formatting, you can check Eneko Alonso answer on Specify number of decimals when serializing currencies with JSONSerialization.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With