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

时间:2017-07-31 15:55:03

标签: json swift serialization double decimal

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例外序列化。

3 个答案:

答案 0 :(得分:8)

在对此问题进行一些研究后,同事发现使用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

答案 1 :(得分:0)

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

您有几种方法可以解决此问题:

  1. 将当前实现保留为Any作为输入参数的类型,但在打印它们之前有条件地将您的值转发到serialize内并使用NumberFormatter()进行打印。
  2. serialize的实施方式更改为通用功能。
  3. 实施3个serialize重载版本,每个版本都接受不同的数字类型并使用确切的类型。
  4. 如果您需要JSON以特定格式包含价格,则应该序列化NumberFormatter的输出而不是数字本身。

答案 2 :(得分:-1)

编码货币金额的策略是通过将该值乘以乘数来将金额转换为整数。 在这种情况下,乘法因子由货币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]}