【问题标题】:Swift JSONEncoder number roundingSwift JSONEncoder 数字舍入
【发布时间】:2020-11-09 17:30:08
【问题描述】:

与所有 IEEE 7540 系统一样,在 Swift 中,4.7 之类的数字被视为4.7000000000000002 之类的值。所以也就不足为奇了:

% swift
Welcome to Apple Swift version 5.2.4 (swiftlang-1103.0.32.9 clang-1103.0.32.53).
Type :help for assistance.
  1> 4.7
$R0: Double = 4.7000000000000002
  2> 4.7 == 4.7000000000000002
$R1: Bool = true

这是一个众所周知的现实,因此不需要使用包含指向浮点精度损失的背景文章链接的 cmets 来解决。

当使用内置的JSONEncoder 编码这个数字时,我们看到:

  4> String(data: JSONEncoder().encode([4.7]), encoding: .utf8) 
$R2: String? = "[4.7000000000000002]"

这并没有错,正如 Wikipedia 所说的 this 关于 JSON 和浮点数:

JSON 标准对上溢、下溢、精度损失、舍入或有符号零等实现细节没有任何要求,但它确实建议期望不超过 IEEE 754 binary64 精度以实现“良好的互操作性”。将浮点数的机器级二进制表示(如 binary64)序列化为人类可读的十进制表示(如 JSON 中的数字)并返回,并没有固有的精度损失,因为存在已发布的算法可以准确地做到这一点并且是最优的。

但是,其他 JavaScript 环境倾向于对这些数字进行四舍五入。例如。使用 JavaScriptCore:

% /System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Helpers/jsc

>>> 4.7 == 4.7000000000000002
true
>>> JSON.stringify([4.7000000000000002])
[4.7]

与节点:

% node
Welcome to Node.js v13.13.0.
Type ".help" for more information.
> 4.7 == 4.7000000000000002
true
> JSON.stringify([4.7000000000000002])
'[4.7]'

对我来说,问题是我有大量 Swift doubles 集合,当序列化为 JSON 用于存储和/或传输时,包含很多不必要的箔条(“4.7000000000000002”的字符比“4.7”多 6 倍),因此大大增加了序列化数据的大小。

谁能想到一个很好的方法来覆盖 Swift 的数字编码以将双精度数序列化为它们的舍入等效值,而不是放弃自动合成可编码性并手动重新实现整个类型图的编码?

【问题讨论】:

  • 你能用Decimal代替Double吗?
  • 这里有类似的观察:stackoverflow.com/q/46271842/1187415.
  • @MartinR 你很快就会失去Decimal 精度和JSONDecoder/JSONDecoder,因为两者实际上都在后台使用JSONSerialization,它只能将浮点数解析为@ 987654338@,它无法处理Decimal。见SR-7054
  • @LeoDabus 是的,这不是一项简单的任务,因为您几乎需要创建自己的 EncoderDecoder 来支持 JSON 而不依赖于 JSONSerialization。我想苹果本身仍然没有解决这个问题是有原因的,即使他们已经知道这个问题至少 2.5 年了。

标签: json swift


【解决方案1】:

您可以扩展 KeyedEncodingContainer 和 KeyedDecodingContainer 并实现自定义编码和解码方法以将 Decimal 作为纯数据发送。您只需将编码器/解码器 dataEncodingStrategy 设置为 deferredToData。另一种可能性是对其 base64Data 进行编码和解码,或者将其编码/解码为纯字符串。

extension Numeric {
    var data: Data {
        var bytes = self
        return .init(bytes: &bytes, count: MemoryLayout<Self>.size)
    }
}

extension DataProtocol {
    func decode<T: Numeric>(_ codingPath: [CodingKey], key: CodingKey) throws -> T {
        var value: T = .zero
        guard withUnsafeMutableBytes(of: &value, copyBytes) == MemoryLayout.size(ofValue: value) else {
            throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "The key \(key) could not be converted to a numeric value: \(Array(self))"))
        }
        return value
    }
}

extension KeyedEncodingContainer {
    mutating func encode(_ value: Decimal, forKey key: K) throws {
        try encode(value.data, forKey: key)
    }
    mutating func encodeIfPresent(_ value: Decimal?, forKey key: K) throws {
        guard let value = value else { return }
        try encode(value, forKey: key)
    }
}

extension KeyedDecodingContainer {
    func decode(_ type: Decimal.Type, forKey key: K) throws -> Decimal {
        try decode(Data.self, forKey: key).decode(codingPath, key: key)
    }
    func decodeIfPresent(_ type: Decimal.Type, forKey key: K) throws -> Decimal? {
        try decodeIfPresent(Data.self, forKey: key)?.decode(codingPath, key: key)
    }
}

游乐场测试:

struct Root: Codable {
    let decimal: Decimal
}

// using the string initializer for decimal is required to maintain precision
let root = Root(decimal: Decimal(string: "0.007")!)

do {
    let encoder = JSONEncoder()
    encoder.dataEncodingStrategy = .deferredToData
    let rootData = try encoder.encode(root)
    let decoder = JSONDecoder()
    decoder.dataDecodingStrategy = .deferredToData
    let root = try decoder.decode(Root.self, from: rootData)
    print(root.decimal) // prints "0.007\n" instead of "0.007000000000000001024\n" without the custom encoding and decoding methods
} catch {
    print(error)
}

为了保持数据大小尽可能小您可以将 Decimal 编码和解码为字符串:

extension String {
    func decimal(_ codingPath: [CodingKey], key: CodingKey) throws -> Decimal {
        guard let decimal = Decimal(string: self) else {
            throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "The key \(key) could not be converted to decimal: \(self)"))
        }
        return decimal
    }

}

extension KeyedEncodingContainer {
    mutating func encode(_ value: Decimal, forKey key: K) throws {
        try encode(String(describing: value), forKey: key)
    }
    mutating func encodeIfPresent(_ value: Decimal?, forKey key: K) throws {
        guard let value = value else { return }
        try encode(value, forKey: key)
    }
}

extension KeyedDecodingContainer {
    func decode(_ type: Decimal.Type, forKey key: K) throws -> Decimal {
        try decode(String.self, forKey: key).decimal(codingPath, key: key)
    }
    func decodeIfPresent(_ type: Decimal.Type, forKey key: K) throws -> Decimal? {
        try decodeIfPresent(String.self, forKey: key)?.decimal(codingPath, key: key)
    }
}

游乐场测试:

struct StringDecimal: Codable {
    let decimal: Decimal
}

let root = StringDecimal(decimal: Decimal(string: "0.007")!)
do {
    let stringDecimalData = try JSONEncoder().encode(root)
    print(String(data: stringDecimalData, encoding: .utf8)!)
    let stringDecimal = try JSONDecoder().decode(StringDecimal.self, from: stringDecimalData)
    print(stringDecimal.decimal) // "0.007\n"
} catch {
    print(error)
}

这将打印出来

{"十进制":"0.007"}
0.007

【讨论】:

  • Decimal 默认符合Codable
  • @vadian 但据我所知它失去了精度
  • 啊,我明白了
  • 这很好地解决了这个问题,感谢 Leo!想知道是否值得为此提出针对 Swift 标准库的 PR。
  • 当然这是假设你的小数是你结构的属性
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-19
相关资源
最近更新 更多