NumberFormatter
makes it quite easy to format currencies when presenting values on screen:
let decimal = Decimal(25.99)
let decimalNumberFormatter = NumberFormatter()
decimalNumberFormatter.numberStyle = .currencyAccounting
let output = decimalNumberFormatter.string(for: decimal)
// output = "$25.99"
The above code works well both for any Decimal
or Double
values. The amount of decimal digits always matches that of the locale being used.
Turns our that serializing a floating point currency value to JSON is not that trivial.
Having the following serializing method (mind the force unwraps):
func serialize(prices: Any...) {
let data = try! JSONSerialization.data(withJSONObject: ["value": prices], options: [])
let string = String(data: data, encoding: .utf8)!
print(string)
}
We can then call it with different values and types. Double
, Decimal
and NSDecimalNumber
(which should be bridged from Swift's Decimal
) fail to properly render the value in some cases.
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]}
I'm not looking to serialize numbers as currencies (no need for currency symbol, integers (5
) or single decimal position (0.3
) are fine). However I'm looking for a solution where the serialized output contains no more than the number of decimals allowed by a given currency (locale).
This is, is there any way to limit or specify the number of decimals to be used when serializing floating point values to JSON?
Update #1:
Tested with more data types, surprisingly seems like both Float
and Float32
work well for two-decimal currencies. Float64
fails as Double
(probably they are an alias of the same type).
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]}
Hard to know if they work well in all cases, though. Float80
fails to serialize with a _NSJSONWriter
exception.
After doing some research in this matter, a coworker found that rounding the values specifying a behavior using NSDecimalNumberHandler
solves the JSON serialization issue.
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
}
}
Following the example code from the post, we get the desired output:
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]}
It works! Ran a test for 10000 values (from 0.0 to 99.99) and found no issues.
If needed, the scale can be adjusted to the number of decimals from the current locale:
var currencyFormatter = NumberFormatter()
currencyFormatter.numberStyle = .currencyAccounting
let scale = currencyFormatter.maximumFractionDigits
// scale == 2
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With