浮点数表示背后的奥秘
Mystery behind presentation of Floating Point numbers
我正在为我的应用程序测试一些简单的解决方案,然后我 运行 遇到了一些问题出现在我脑海中的情况...
"Why one floating number is represented in JSON correctly (as I expect) and other one not...?"
在这种情况下,从 String 转换为 Decimal,然后转换为 JSON of number: "98.39" 从人类的角度来看是完全可以预测的,但是 number: "98.40" 看起来并不那么漂亮.. .
我的问题是,有人可以向我解释一下吗,为什么从字符串到十进制的转换对一个浮点数如我所料,但对另一个则不然。
我对浮点数错误有很多了解,但我无法弄清楚如何处理
String ->...基于二进制的转换内容...-> 到 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";
}
------------------------------------------------
*/
我 looking/trying 弄清楚要转换的逻辑单元执行了哪些步骤:
"98.39" -> 十进制 -> 字符串 - 结果为 "98.39"
并具有相同的转换链:
"98.40" -> 十进制 -> 字符串 - 结果为 "98.40000000000001"
非常感谢所有回复!
似乎在某些时候,JSON 表示将值存储为二进制浮点数。
特别是,最接近 98.40 的 double
(IEEE binary64) 值为 98.400000000000005684341886080801486968994140625,四舍五入到 16 位有效数字时为 98.40000000000001。
为什么是16位有效数字?这是一个很好的问题,因为 16 位有效数字不足以唯一标识所有浮点值,例如0.056183066649934776
和0.05618306664993478
等同于16位有效数字,但对应不同的值。奇怪的是你的代码现在打印
["amount": 0.056183066649934998]
两者都是 17 位有效数字,但实际上是一个完全 错误的 值,相差 32 units in the last place。我不知道那里发生了什么。
有关二进制-十进制转换所需位数的详细信息,请参见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("%f\n",f);
printf("%lf\n",d);
return(1);
}
98.389999
98.390000
正如西蒙指出的那样,这根本不是一个谜。这就是计算机的工作原理,您正在使用 base 2 机器来执行 base 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
只要看一下它们之间的尾数,就可以清楚地知道这不会解析为一个确切的数字,因此四舍五入和更多四舍五入的格式化打印开始发挥作用...
这纯粹是 NSNumber
如何打印自身的产物。
JSONSerialization
在 Objective-C 中实现并使用 Objective-C 对象(NSDictionary
、NSArray
、NSString
、NSNumber
等)来表示它从您的 JSON 中反序列化的值。由于 JSON 包含一个带小数点的裸数字作为 "amount"
键的值,JSONSerialization
将其解析为 double
并将其包装在 NSNumber
中.
这些 Objective-C 类 中的每一个都实现了一个 description
方法来打印自己。
JSONSerialization
返回的对象是一个 NSDictionary
。 String(describing:)
通过发送 description
方法将 NSDictionary
转换为 String
。 NSDictionary
通过向其每个键和值发送 description
来实现 description
,包括 "amount"
键的 NSNumber
值。
description
的 NSNumber
实现使用 printf
说明符 %0.16g
格式化 double
值。 (我使用反汇编程序进行了检查。)关于 g
说明符,C 标准说
Finally, unless the # flag is used, any trailing zeros are removed from the fractional portion of the result and the decimal-point wide character is removed if there is no fractional portion remaining.
最接近 98.39 的 double 恰好是 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 的 double 正好是 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:
的行上)和 Swift Double
版本在所有情况下都使用最少的数字。使用 -[NSNumber description]
的行(标记为 jso:
和 amount as NSNumber:
)对某些值使用了额外的数字。
我正在为我的应用程序测试一些简单的解决方案,然后我 运行 遇到了一些问题出现在我脑海中的情况... "Why one floating number is represented in JSON correctly (as I expect) and other one not...?"
在这种情况下,从 String 转换为 Decimal,然后转换为 JSON of number: "98.39" 从人类的角度来看是完全可以预测的,但是 number: "98.40" 看起来并不那么漂亮.. .
我的问题是,有人可以向我解释一下吗,为什么从字符串到十进制的转换对一个浮点数如我所料,但对另一个则不然。
我对浮点数错误有很多了解,但我无法弄清楚如何处理 String ->...基于二进制的转换内容...-> 到 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";
}
------------------------------------------------
*/
我 looking/trying 弄清楚要转换的逻辑单元执行了哪些步骤: "98.39" -> 十进制 -> 字符串 - 结果为 "98.39" 并具有相同的转换链: "98.40" -> 十进制 -> 字符串 - 结果为 "98.40000000000001"
非常感谢所有回复!
似乎在某些时候,JSON 表示将值存储为二进制浮点数。
特别是,最接近 98.40 的 double
(IEEE binary64) 值为 98.400000000000005684341886080801486968994140625,四舍五入到 16 位有效数字时为 98.40000000000001。
为什么是16位有效数字?这是一个很好的问题,因为 16 位有效数字不足以唯一标识所有浮点值,例如0.056183066649934776
和0.05618306664993478
等同于16位有效数字,但对应不同的值。奇怪的是你的代码现在打印
["amount": 0.056183066649934998]
两者都是 17 位有效数字,但实际上是一个完全 错误的 值,相差 32 units in the last place。我不知道那里发生了什么。
有关二进制-十进制转换所需位数的详细信息,请参见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("%f\n",f);
printf("%lf\n",d);
return(1);
}
98.389999
98.390000
正如西蒙指出的那样,这根本不是一个谜。这就是计算机的工作原理,您正在使用 base 2 机器来执行 base 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
只要看一下它们之间的尾数,就可以清楚地知道这不会解析为一个确切的数字,因此四舍五入和更多四舍五入的格式化打印开始发挥作用...
这纯粹是 NSNumber
如何打印自身的产物。
JSONSerialization
在 Objective-C 中实现并使用 Objective-C 对象(NSDictionary
、NSArray
、NSString
、NSNumber
等)来表示它从您的 JSON 中反序列化的值。由于 JSON 包含一个带小数点的裸数字作为 "amount"
键的值,JSONSerialization
将其解析为 double
并将其包装在 NSNumber
中.
这些 Objective-C 类 中的每一个都实现了一个 description
方法来打印自己。
JSONSerialization
返回的对象是一个 NSDictionary
。 String(describing:)
通过发送 description
方法将 NSDictionary
转换为 String
。 NSDictionary
通过向其每个键和值发送 description
来实现 description
,包括 "amount"
键的 NSNumber
值。
description
的 NSNumber
实现使用 printf
说明符 %0.16g
格式化 double
值。 (我使用反汇编程序进行了检查。)关于 g
说明符,C 标准说
Finally, unless the # flag is used, any trailing zeros are removed from the fractional portion of the result and the decimal-point wide character is removed if there is no fractional portion remaining.
最接近 98.39 的 double 恰好是 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 的 double 正好是 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:
的行上)和 Swift Double
版本在所有情况下都使用最少的数字。使用 -[NSNumber description]
的行(标记为 jso:
和 amount as NSNumber:
)对某些值使用了额外的数字。