Search code examples
jsonswiftalamofirensdecimalnumber

AlamoFire responseJSON decimal precision


I'm using AlamoFire to call my web service:

ApiManager.manager.request(.GET, webServiceCallUrl,  parameters: ["id": 123])
        .validate()
        .responseJSON { response in
            switch response.result {
            case .Success:
                print(response.result.value!)

                //...

            case .Failure:
                //...
            }
    }

My web service returns the following JSON:

{
    //...
    "InvoiceLines": [{
        "Amount": 0.94
    }]
}

Alamofire is treating this as a double instead of a decimal so in the output console I get:

{
    //...
    InvoiceLines =     (
                {
            Amount = "0.9399999999999999";
        }
    );
}

This then causes rounding errors further down in my code.

I've used Fiddler on the server to inspect the web service JSON response to confirm that it is returning 0.94. So from that I can rule out the server being the issue and suspect the responseJSON causing my issue.

How can I get currency values to return as the correct NSDecimalNumber value?


Extra information after Jim's answer/comments:

var stringToTest = "{\"Amount\":0.94}"
var data = stringToTest.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
var object = try NSJSONSerialization.JSONObjectWithData(data!, options: opt)

var a = object["Amount"]
print(a) //"Optional(0.9399999999999999)"

var b = NSDecimalNumber(decimal: (object["Amount"] as! NSNumber).decimalValue)
print(b) //"0.9399999999999999"

If I pass through the value as a string in the JSON I can then get my desired result:

var stringToTest = "{\"Amount\":\"0.94\"}"
var data = stringToTest.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
var object = try NSJSONSerialization.JSONObjectWithData(data!, options: opt)

var c = NSDecimalNumber(string: (object["Amount"] as! String))
print(c) //0.94

However I don't want to have implement changes to the API so need a solution which keeps the JSON in the same format. Is my only option to round the double each time? It just seems feels like the wrong way to do things and potentially raise rounding problems in the future.


Solution

  • Decimal values are themselves floating-point, and the JSON spec specifically allows high-exponent values which you might think of as "real" floating point:

    http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf Section 8 "numbers"

    What you're describing as "decimal" is decimal floating point, in which you have an integer and a base10 exponent, i.e. 94 * 10^-2 in this case. Very few systems support this storage format and instead use binary floating point, which is an integer and a base2 exponent. Since 2 is not a factor of 5, tenths, hundredths et cetera can't be precisely represented in binary floating point. This means that the internal representation is always a little off, but if you try to do comparisons or print it is aware of the precision and will work as you expect.

    var bar = 0.94 // 0.939999999999999
    bar == 0.94 // true
    print(bar) // "0.94\n"
    

    Where you're hitting problems in other code will be due to compounding the precision problem. In decimal floating point, if you try to do 1/3 + 1/3 - 2/3 you get 0.333333 + 0.333333 - 0.666667 = -0.000001 != 0, and in binary floating point you get the same issue. This is because after one or more operations the value will be outside the precision range for converting from the original number, so it is taken literally.

    If you're in this kind of situation with data which is actually fixed point (such as currency values in most cases) the safest approach is to multiply it up then only use integer values until you have to display, as in:

    var bar = 0.94 * 100
    var foo = bar * 5 // multiply price by 5
    print(bar / 100)
    

    Alternatively, since double-precision floating point has a very high base precision, far more than the 1/100 needed, you can just round when performing comparisons or output.

    var bar = 0.94
    var foo = bar * 5 // multiply price by 5
    print(round(bar*100) / 100)