Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSJSONSerialization with small decimal numbers

Tags:

json

swift

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).

like image 644
I Stevenson Avatar asked Jan 28 '16 05:01

I Stevenson


Video Answer


2 Answers

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
}
like image 120
Martin R Avatar answered Sep 28 '22 17:09

Martin R


Manual conversion

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

JSONSerialization extension for automatic conversion

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)
    }
}

Usage

JSONSerialization.decimalData(withJSONObject: [ "value": 0.81 ], options: [])

Note

If you need fine tuning of decimal formatting, you can check Eneko Alonso answer on Specify number of decimals when serializing currencies with JSONSerialization.

like image 29
Cœur Avatar answered Sep 28 '22 17:09

Cœur