浮点数显示背后的谜团

时间:2018-08-29 20:09:58

标签: swift floating-point decimal precision floating-accuracy

我当时正在为我的应用程序测试一些简单的解决方案,但遇到某种情况,我的脑海里浮现了问题... “为什么一个浮点数正确地(如我所期望的那样)用JSON表示,而另一个则不...?”

在这种情况下,从字符串到十进制然后再转换为数字:“ 98.39”的JSON从人类的角度来看是完全可以预测的,但是数字:“ 98.40”看起来并不那么漂亮...

我的问题是,有人可以对我解释一下吗,为什么从String到Decimal的转换可以像我期望的那样对一个浮点数起作用,而对于另一个浮点数却不能。

我对浮点数错误有很多了解,但我无法弄清楚如何处理 字符串-> ...基于二进制的转换内容...->到Double的两种情况下的精度都不同。


我的游乐场代码:

struct Price: Encodable {
    let amount: Decimal
}

func printJSON(from string: String) {
    let decimal = Decimal(string: string)!
    let price = Price(amount: decimal)

    //Encode Person Struct as Data
    let encodedData = try? JSONEncoder().encode(price)

    //Create JSON
    var json: Any?
    if let data = encodedData {
        json = try? JSONSerialization.jsonObject(with: data, options: [])
    }

    //Print JSON Object
    if let json = json {
        print("Person JSON:\n" + String(describing: json) + "\n")
    }
}

let stringPriceOK =     "98.39"
let stringPriceNotOK =  "98.40"
let stringPriceNotOK2 = "98.99"

printJSON(from: stringPriceOK)
printJSON(from: stringPriceNotOK)
printJSON(from: stringPriceNotOK2)
/*
 ------------------------------------------------
 // OUTPUT:
 Person JSON:
 {
 amount = "98.39";
 }

 Person JSON:
 {
 amount = "98.40000000000001";
 }

 Person JSON:
 {
 amount = "98.98999999999999";
 }
 ------------------------------------------------
 */

我正在寻找/试图弄清楚逻辑单元执行了哪些步骤来转换: “ 98.39”->小数->字符串-结果为“ 98.39” 并具有相同的转化链: “ 98.40”->小数->字符串-结果为“ 98.40000000000001”

非常感谢您的所有答复!

3 个答案:

答案 0 :(得分:3)

似乎在某个时候,JSON表示将值存储为二进制浮点。

尤其是,最接近double(IEEE二进制64)值的98.40将是98.400000000000005684341886080801486968994140625,当四舍五入为16个有效数字时,则是98.40000000000001。

为什么有16个有效数字?这是一个很好的问题,因为16个有效数字不足以唯一地标识所有浮点值,例如0.0561830666499347760.05618306664993478与16个有效数字相同,但对应于不同的值。奇怪的是,您的代码现在可以打印

["amount": 0.056183066649934998]
两者的

均为17位有效数字,但实际上完全是错误值,相差32 units in the last place。我不知道那是怎么回事。

有关https://www.exploringbinary.com/number-of-digits-required-for-round-trip-conversions/的详细信息,请参阅二进制十进制转换所需的位数。

答案 1 :(得分:2)

这纯粹是NSNumber如何打印自身的产物。

JSONSerialization在Objective-C中实现,并使用Objective-C对象(NSDictionaryNSArrayNSStringNSNumber等)来表示从JSON反序列化的值。由于JSON包含一个带有小数点的裸数字作为"amount"键的值,因此JSONSerialization将其解析为double并将其包装在NSNumber中。

每个Objective-C类都实现一个description方法来打印自身。

JSONSerialization返回的对象是NSDictionaryString(describing:)通过发送NSDictionary方法将String转换为descriptionNSDictionary通过向其每个键和值(包括description键的description值)发送NSNumber来实现"amount"

NSNumber的{​​{1}}实现使用description说明符double格式化printf值。 (我使用反汇编程序进行了检查。)关于%0.16g说明符,C标准说

  

最后,除非使用#标志,否则将从结果的小数部分中删除所有结尾的零,如果没有剩余的小数部分,则将删除小数点宽字符。

最接近98.39的双精度正好是98.3900000000000005685681881886080801486968994140625。因此g将其格式设置为%0.16g(请参见标准为何为14,而不是16),这样得到%0.14f,然后将尾随的零斩去,得到"98.39000000000000"。 / p>

最接近98.40的双精度数恰好是98.400000000000005684341886080801486966964140625。因此"98.39"的格式设置为%0.16g,这样就得到了%0.14f(由于四舍五入),并且没有尾随的零可以切掉。

因此,当您打印"98.40000000000001"的结果时,对于98.40,您得到很多小数位,但对于98.39,却只有两位数。

如果您从JSON对象中提取金额并将其转换为Swift的本机JSONSerialization.jsonObject(with:options:)类型,然后打印这些Double,则会得到更短的输出,因为Double实现了一个更智能的格式化算法,可打印出最短的字符串,该字符串在经过分析后会产生与Double完全相同的字符串。

尝试一下:

Double

结果:

import Foundation

struct Price: Encodable {
    let amount: Decimal
}

func printJSON(from string: String) {
    let decimal = Decimal(string: string)!
    let price = Price(amount: decimal)

    let data = try! JSONEncoder().encode(price)
    let jsonString = String(data: data, encoding: .utf8)!
    let jso = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
    let nsNumber = jso["amount"] as! NSNumber
    let double = jso["amount"] as! Double

    print("""
    Original string: \(string)
        json: \(jsonString)
        jso: \(jso)
        amount as NSNumber: \(nsNumber)
        amount as Double: \(double)

    """)
}

printJSON(from: "98.39")
printJSON(from: "98.40")
printJSON(from: "98.99")

请注意,在所有情况下,实际JSON(在标记为Original string: 98.39 json: {"amount":98.39} jso: ["amount": 98.39] amount as NSNumber: 98.39 amount as Double: 98.39 Original string: 98.40 json: {"amount":98.4} jso: ["amount": 98.40000000000001] amount as NSNumber: 98.40000000000001 amount as Double: 98.4 Original string: 98.99 json: {"amount":98.99} jso: ["amount": 98.98999999999999] amount as NSNumber: 98.98999999999999 amount as Double: 98.99 的行上)和Swift json:版本都使用最少的数字。使用Double(标记为-[NSNumber description]jso:)的行为某些值使用多余的数字。

答案 2 :(得分:0)

#include <stdio.h>
int main ( void )
{
    float f;
    double d;

    f=98.39F;
    d=98.39;

    printf("%f\n",f);
    printf("%lf\n",d);
    return(1);
}
98.389999
98.390000
正如西蒙指出的,

这根本不是一个谜。它就是计算机的工作方式,您正在使用2台计算机来处理10台计算机。就像1/3是一个非常简单的数字,但以10为底的数字是0.3333333。永远,不准确也不漂亮,但以3为底,它会像0.1一样漂亮干净。例如,以10为基数的数字与以2为基数1/10的数字不能很好地匹配。

float fun0 ( void )
{
    return(98.39F);
}
double fun1 ( void )
{
    return(98.39);
}
00000000 <fun0>:
   0:   e59f0000    ldr r0, [pc]    ; 8 <fun0+0x8>
   4:   e12fff1e    bx  lr
   8:   42c4c7ae    sbcmi   ip, r4, #45613056   ; 0x2b80000

0000000c <fun1>:
   c:   e59f0004    ldr r0, [pc, #4]    ; 18 <fun1+0xc>
  10:   e59f1004    ldr r1, [pc, #4]    ; 1c <fun1+0x10>
  14:   e12fff1e    bx  lr
  18:   c28f5c29    addgt   r5, pc, #10496  ; 0x2900
  1c:   405898f5    ldrshmi r9, [r8], #-133 ; 0xffffff7b

42c4c7ae  single
405898f5c28f5c29  double

0 10000101 10001001100011110101110
0 10000000101 1000100110001111010111000010100011110101110000101001

10001001100011110101110
1000100110001111010111000010100011110101110000101001

只是清楚地看一下它们之间的尾数,这不会解决确切的数字,因此,四舍五入和带更多舍入的格式化打印才开始起作用...