12

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

I Stevenson
  • 854
  • 1
  • 7
  • 24

3 Answers3

12

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
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
2

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.

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • 1
    "((0.81 as NSNumber).decimalValue)" returns "0.8100000000000001" Xcode 11.3.1 – Anton Plebanovich Feb 13 '20 at 12:13
  • 1
    @AntonPlebanovich oh, something may have changed in 2 years. Well, I've edited the message with a different workaround. – Cœur Feb 15 '20 at 08:10
  • It looks like we need to use `scale: 13`. Otherwise for `32.98` with rounding `15` it produces `32.979999999999995` and `32.97999999999999` with rounding `14` – Anton Plebanovich Feb 11 '23 at 14:58
  • It looks like we need to use a dynamic rounding scale that depends on the integral part size actually. For `123123123.123213` it produces `123123123.1232130048` – Anton Plebanovich Feb 11 '23 at 15:07
0

If you have use 'NSDecimalNumber' demand, it is suggested that encapsulate for ease of use and reduce mistakes.

Here's the Demo for you reference, using a simple.The hope can help you!

switch (operatorType) {
  case 0:
      resultNumber = SNAdd(_cardinalNumberTextField.text, _complementNumberTextField.text);
      break;
  case 1:
      resultNumber = SNSub(_cardinalNumberTextField.text, _complementNumberTextField.text);
      break;
  case 2:
      resultNumber = SNMul(_cardinalNumberTextField.text, _complementNumberTextField.text);
      break;
  case 3:
      resultNumber = SNDiv(_cardinalNumberTextField.text, _complementNumberTextField.text);
      break;
 }

Github:https://github.com/ReverseScale/DecimalNumberDemo

Tim
  • 1,528
  • 1
  • 11
  • 8