使用 JSONSerialization 序列化货币时指定小数位数

Posted

技术标签:

【中文标题】使用 JSONSerialization 序列化货币时指定小数位数【英文标题】:Specify number of decimals when serializing currencies with JSONSerialization 【发布时间】:2017-07-31 15:55:02 【问题描述】:

NumberFormatter 在屏幕上显示值时可以很容易地格式化货币:

let decimal = Decimal(25.99)
let decimalNumberFormatter = NumberFormatter()
decimalNumberFormatter.numberStyle = .currencyAccounting
let output = decimalNumberFormatter.string(for: decimal)
// output = "$25.99"

上述代码适用于任何DecimalDouble 值。小数位数始终与所使用的语言环境相匹配。

证明将浮点货币值序列化为 JSON 并不是那么简单。

具有以下序列化方法(注意强制解包):

func serialize(prices: Any...) 
    let data = try! JSONSerialization.data(withJSONObject: ["value": prices], options: [])
    let string = String(data: data, encoding: .utf8)!
    print(string)

然后我们可以用不同的值和类型来调用它。 DoubleDecimalNSDecimalNumber(应该从 Swift 的 Decimal 桥接)在某些情况下无法正确呈现值。

serialize(prices: 125.99, 16.42, 88.56, 88.57, 0.1 + 0.2)
// "value":[125.99,16.42,88.56,88.56999999999999,0.3]

serialize(prices: Decimal(125.99), Decimal(16.42), Decimal(88.56), Decimal(88.57), Decimal(0.1) + Decimal(0.2))
// "value":[125.98999999999997952,16.420000000000004096,88.56,88.57,0.3]

serialize(prices: NSDecimalNumber(value: 125.99), NSDecimalNumber(value: 16.42), NSDecimalNumber(value: 88.56), NSDecimalNumber(value: 88.57), NSDecimalNumber(value: 0.1).adding(NSDecimalNumber(value: 0.2)))
// "value":[125.98999999999997952,16.420000000000004096,88.56,88.57,0.3]

我不希望将数字序列化为货币(不需要货币符号、整数 (5) 或单个小数位 (0.3) 都可以)。但是我正在寻找一种解决方案,其中序列化输出包含的小数位数不超过给定货币所允许的小数位数(语言环境)。

这是,有没有办法限制或指定将浮点值序列化为 JSON 时使用的小数位数?

更新 #1: 经过更多数据类型的测试,令人惊讶的是,FloatFloat32 似乎都适用于两位十进制货币。 Float64 失败为 Double(可能它们是同一类型的别名)。

serialize(prices: Float(125.99), Float(16.42), Float(88.56), Float(88.57), Float(0.1) + Float(0.2))
// "value":[125.99,16.42,88.56,88.57,0.3]

serialize(prices: Float32(125.99), Float32(16.42), Float32(88.56), Float32(88.57), Float32(0.1) + Float32(0.2))
// "value":[125.99,16.42,88.56,88.57,0.3]

serialize(prices: Float64(125.99), Float64(16.42), Float64(88.56), Float64(88.57), Float64(0.1) + Float64(0.2))
// "value":[125.99,16.42,88.56,88.56999999999999,0.3]

但很难知道它们是否在所有情况下都能正常工作。 Float80 序列化失败,出现_NSJSONWriter 异常。

【问题讨论】:

关于这一行:I'm looking for a solution where the serialized output contains no more than the number of decimals allowed by a given currency,这是否意味着您想要对 JSON 进行不可逆编码?例如。如果某个金额(例如 $5.75 以仅需要一位小数点精度的货币编码),则应将其编码为 5.7 并且 $0.05 将丢失。 理想情况下,它将依赖于当前语言环境货币允许的maximumFractionDigits,因此不会丢失小数。 任何关于它如何以相反方式工作的想法,即具有值10 的小数被序列化。可以使用此方法将其序列化为10.0吗? 【参考方案1】:

在对此事进行一些研究后,一位同事发现使用 NSDecimalNumberHandler 对指定行为的值进行四舍五入可以解决 JSON 序列化问题。

fileprivate let currencyBehavior = NSDecimalNumberHandler(roundingMode: .bankers, scale: 2, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: true)

extension Decimal 
    var roundedCurrency: Decimal 
        return (self as NSDecimalNumber).rounding(accordingToBehavior: currencyBehavior) as Decimal
    

按照帖子中的示例代码,我们得到了所需的输出:

serialize(prices: Decimal(125.99).roundedCurrency, Decimal(16.42).roundedCurrency, Decimal(88.56).roundedCurrency, Decimal(88.57).roundedCurrency, (Decimal(0.1) + Decimal(0.2)).roundedCurrency)
// "value":[125.99,16.42,88.56,88.57,0.3]

有效!对 10000 个值(从 0.0 到 99.99)进行了测试,没有发现任何问题。

如果需要,可以将比例调整为当前语言环境的小数位数:

var currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currencyAccounting
let scale = currencyFormatter.maximumFractionDigits
// scale == 2

【讨论】:

【参考方案2】:

问题是您使用Any 作为函数的可变输入参数,而不是使用通用函数/重载函数。这样,通过向上转换到 Any 来掩盖确切的类型信息。

你有几种方法可以解决这个问题:

    保持当前实现使用 Any 作为输入参数的类型,但在打印之前有条件地向下转换 serialize 中的值并使用 NumberFormatter() 进行打印。 将serialize 的实现更改为通用函数。 实现 serialize 的 3 个重载版本,每个版本都接受不同的数字类型并使用确切的类型。

如果您需要您的JSON 以某种格式包含您的价格,您应该序列化NumberFormatter 的输出而不是数字本身。

【讨论】:

JSONSerializationAny 用于字典序列化。无论您对字典值使用什么类型([String: Double][String: Decimal]...),浮点​​精度问题仍然存在。见developer.apple.com/documentation/foundation/jsonserialization/… 正如我在上一句中所说,如果您需要 JSON 本身包含具有固定格式的数字,您必须序列化数字的字符串表示形式而不是数字本身。【参考方案3】:

对货币金额进行编码的一种策略是通过将值乘以乘法因子将金额转换为整数。 在这种情况下,乘法因子由 10 的货币 maximumFractionDigits 的幂次方给出(例如,在小数部分使用两位数的货币为 10^2)。

请记住,这种方法仅适用于存储准备显示给用户的金额。见here for details。

func serialize(prices: Double..., locale: Locale) 

    let formatter = NumberFormatter()
    formatter.locale = locale
    formatter.numberStyle = .currencyAccounting

    // We multiply the amount by as many orders of 
    // magnitude are needed to ensure it is an integer.
    // In your implementation, you should store the value of
    // maximumFractionDigits along with the amount to ensure
    // you can always recover the correct value.
    let items = prices.map 
        $0 * pow(10, Double(formatter.maximumFractionDigits))
    

    let data = try! JSONSerialization.data(withJSONObject: ["value": items], options: [])
    let string = String(data: data, encoding: .utf8)!
    print(string)


print(serialize(prices: 125.99, 16.42, 88.56, 88.57, 0.1 + 0.2, locale: Locale(identifier: "en_AU")))

打印:

"value":[12599,1642,8856,8857,30]

【讨论】:

同意,多年来使用整数一直是一个很好的解决方案。但是,它要求接收 JSON 的 API 也需要整数。目前,我正在使用的 API 预计实际金额,并且无法指定乘数。

以上是关于使用 JSONSerialization 序列化货币时指定小数位数的主要内容,如果未能解决你的问题,请参考以下文章

JSONSerialization 没有在服务器发送时序列化数据

添加嵌套字典会导致 JSONSerialization 返回 nil

Swift - JSONSerialization 无效的 JSON

Swift - JSONSerialization 无效的 JSON

44-Swift 之 JSONSerialization

在 swift 中使用 Alamofire 的 JsonSerialization 错误