浮点数呈现背后的奥秘



我正在为我的应用程序测试一些简单的解决方案,我遇到了一些问题出现在我脑海中的情况...... "为什么一个浮点数在 JSON 中正确表示(如我所料)而另一个没有......?">

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

我的问题是,有人可以向我解释一下,为什么从字符串到十进制的转换对一个浮点数的预期是有效的,但对于另一个浮点数则不是。

我对浮点数错误有很多红色,但我无法弄清楚过程是如何从 字符串 ->...基于二进制的转换内容...->到双精度对于这两种情况都有不同的精度。


我的游乐场代码:

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">

非常感谢所有回复!

这纯粹是NSNumber如何打印自身的工件。

JSONSerialization

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

这些 Objective-C 类中的每一个都实现了description方法来打印自身。

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

descriptionNSNumber实现使用printf说明符%0.16g格式化double值。(我使用反汇编器进行了检查。关于g说明符,C 标准说

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

最接近 98.39 的双精度正好是 98.3900 0000 0000 0005 6843 4188 6080 8014 8696 8994 1406 25。所以%0.16g将其格式化为%0.14f(请参阅标准以了解为什么它是 14,而不是 16),这给出了"98.39000000000000",然后砍掉尾随的零,给出"98.39"

最接近 98.40 的双精度正好是 98.4000 0000 0000 0056 8434 1886 0808 0148 6968 9941 4062 5。因此,%0.16g将其格式化为%0.14f,这给出了"98.40000000000001"(因为四舍五入),并且没有尾随零要砍掉。

所以这就是为什么,当你打印JSONSerialization.jsonObject(with:options:)的结果时,你会得到很多98.40的小数位数,但98.39只有两位数。

如果从 JSON 对象中提取数量并将其转换为 Swift 的本机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")

结果:

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

请注意,实际的 JSON(在标记为json:的行上)和 SwiftDouble版本在所有情况下都使用最少的数字。使用-[NSNumber description]的行(标记为jso:amount as NSNumber:)对某些值使用额外的数字。

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

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

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

["amount": 0.056183066649934998]

对于两者,这是 17 个有效数字,但实际上是一个完全错误的值,在最后位置下降了 32 个单位。我不知道那里发生了什么。

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

#include <stdio.h>
int main ( void )
{
float f;
double d;
f=98.39F;
d=98.39;
printf("%fn",f);
printf("%lfn",d);
return(1);
}
98.389999
98.390000

正如西蒙指出的那样,这根本不是一个谜。 它只是计算机的工作方式,您使用 Base 2 机器来做 Base 10 的事情。 就像 1/3 是一个非常简单的数字,但在十进制中它是 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

只是清楚地看着它们之间的尾数,这不会解析到一个确切的数字,所以四舍五入和格式化印刷与更多的舍入开始发挥作用......

最新更新