AlamoFire响应JSON小数精度

3

我正在使用AlamoFire调用我的Web服务:

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

                //...

            case .Failure:
                //...
            }
    }

我的网络服务返回以下JSON:

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

Alamofire将其视为double而不是十进制数,因此在输出控制台中我会得到:

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

这会导致我的代码中出现舍入误差。
我已在服务器上使用Fiddler检查了Web服务JSON响应,以确认它返回的是0.94。因此,我可以排除服务器的问题,并怀疑responseJSON引起了我的问题。
我如何使货币值返回正确的NSDecimalNumber值?

Jim回答/评论后的额外信息:
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"

如果我将值以JSON字符串的形式传递,那么我就可以得到我想要的结果:
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

然而,我不想对API进行任何更改,因此需要一种将JSON保持在相同格式的解决方案。我的唯一选择是每次四舍五入吗?这似乎不是正确的做法,并且可能会在未来引起舍入问题。

1个回答

2

十进制的值本身是浮点数,JSON规范明确允许高指数值,可以认为是“真实”的浮点数。

http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf 第8节 “数字”

你所描述的“十进制”是十进制浮点数,在这种情况下,您有一个整数和一个基于10的指数,例如此例中的94 * 10 ^ -2。极少数系统支持此存储格式,而是使用二进制浮点数,其中包含一个整数和一个基于2的指数。由于2不是5的因数,十分之一,百分之一等无法在二进制浮点数中精确表示。这意味着内部表示总是有所偏差,但如果您尝试进行比较或打印,则它会知道精度并按预期工作。

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

在其他代码中出现问题的原因是复合精度问题。在十进制浮点数中,如果尝试执行1/3 + 1/3 - 2/3,则会得到0.333333 + 0.333333 - 0.666667 = -0.000001 != 0,而在二进制浮点数中也会出现相同的问题。这是因为经过一次或多次操作后,值将超出从原始数字转换的精度范围,因此它被视为字面值。
如果您遇到实际上是定点数的数据(例如大多数情况下的货币值),最安全的方法是将其乘以整数值,直到必须显示为止,如下所示:
var bar = 0.94 * 100
var foo = bar * 5 // multiply price by 5
print(bar / 100)

另外,由于双精度浮点数具有非常高的基本精度,远超过所需的1/100,因此在执行比较或输出时,您可以直接四舍五入。

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

感谢您的详细解释。我对浮点数背后发生的事情有了一定的理解。我们在应用程序中使用NSDecimalNumber,这在其他地方没有引起问题 - 改变整个应用程序以将计算乘以整数值不是一个选项。我可以简单地将NSDecimalNumber设置为所需的格式,但我进行了进一步的测试,发现NSJSONSerialization对于0.93、0.95、0.96、0.97返回正常。只有0.94表现不同,它显示为0.939999999999999,因此我正在努力理解为什么0.94的行为不同。 - Joseph
除非数字的内部表示不足以匹配自身,否则您真的不需要担心数字的内部表示。无论如何,您可以在 playground 中直接尝试所有这些内容,例如:`var a = 0.93 // 0.93` `var b = 0.94 // 0.93999999999` `var c = 0.95 // 0.95`只是对于某些数字,内部 var 转储例程处理转换为文本时可能会出现问题;可能对于该数字,内部表示为 0.93999999...995,并且存在一个轻微的错误导致 var 转储处理程序向下舍入而不是向上舍入。只要 print() 起作用,您可以忽略它。 - Jim Driscoll
我遇到的问题是,当前使用的方式下,print() 输出了错误的值。由于在游乐场里已经折磨了自己2个小时,所以我添加了一些额外的代码来解决这个问题。 - Joseph
乘法和除法。print(NSDecimalNumber(double:round(0.94 * 100)).decimalNumberByDividingBy(100))可以正常工作,而print(NSDecimalNumber(double: 0.94))则不行。看起来可能存在一些类型推断问题。请注意,前者的内部转储是不准确的,但实际输出是正常的。 - Jim Driscoll

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接