Swift JSONEncoder数字舍入 [英] Swift JSONEncoder number rounding

查看:110
本文介绍了Swift JSONEncoder数字舍入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

与所有IEEE 7540系统一样,Swift中的数字(如4.7)被视为类似于4.7000000000000002的值.因此,不足为奇:

As with all IEEE 7540 systems, a number in Swift like 4.7 is treated as a value like 4.7000000000000002. So it isn't surprising that:

% 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

这是世界上一个容易理解的现实,因此不需要用包含指向浮点精度损失的背景文章链接的注释来解决.

This is a well-understood reality of the world, and so does not need to be addressed with comments containing links to background articles on floating-point precision loss.

使用内置的JSONEncoder对该数字进行编码时,我们看到:

When encoding this number using the built-in JSONEncoder, we see:

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

这是不正确的,因为Wikipedia说关于JSON&的.浮点数:

This is not incorrect, as Wikipedia says this about JSON & floating point numbers:

JSON标准对实现细节没有要求,例如上溢,下溢,精度损失,舍入或有符号的零,但是对于良好的互操作性",建议不要超过IEEE 754 binary64精度.将浮点数的机器级二进制表示形式(如binary64)序列化为人类可读的十进制表示形式(如JSON中的数字),然后返回,不会固有的精度损失,因为已有发布的算法可以精确地做到这一点和最佳.

The JSON standard makes no requirements regarding implementation details such as overflow, underflow, loss of precision, rounding, or signed zeros, but it does recommend to expect no more than IEEE 754 binary64 precision for "good interoperability". There is no inherent precision loss in serializing a machine-level binary representation of a floating-point number (like binary64) into a human-readable decimal representation (like numbers in JSON), and back, since there exist published algorithms to do this exactly and optimally.

但是,其他JavaScript环境倾向于将这些数字取整.例如.使用JavaScriptCore:

However, other JavaScript environments tend to round these numbers. E.g. with 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双打,当序列化为JSON进行存储和/或传输时,它们包含很多不必要的谷壳("4.7000000000000002"的字符多于"4.7"的6倍) ;),从而大大扩大了序列化数据的大小.

The problem for me is that I have large collections of Swift doubles that, when serialized to JSON for storage and/or transmission, contain a lot of unnecessary chaff ("4.7000000000000002" has 6x more characters than "4.7"), thereby inflating the size of the serialized data considerably.

有人能想到一种很好的方法来覆盖Swift的数字编码以将double序列化为四舍五入的等效序列,而不是放弃可编码性的自动综合并手动重新实现整个类型图的编码吗?

Can anyone think of a nice way to override Swift's numeric encoding to serialize doubles as their rounded equivalent, short of giving up on auto-synthesis of encodability and re-implementing the encoding of the entire type graph manually?

推荐答案

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

You can extend KeyedEncodingContainer and KeyedDecodingContainer and implement a custom encoding and decoding methods to send Decimal as plain data. You would just need to set the encoder/decoder dataEncodingStrategy to deferredToData. Another possibility is to encode and decode its base64Data or encode/decode it as plain string.

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)
    }
}


游乐场测试:


Playground testing:

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编码和解码为字符串:


To keep the data size as low as possible You can encode and decode Decimal as string:

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)
    }
}


游乐场测试:


Playground testing:

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)
}


这将打印


This will print

{十进制":"0.007"}
0.007

{"decimal":"0.007"}
0.007

这篇关于Swift JSONEncoder数字舍入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆