Making NSDecimalNumber Codable
It is not possible to extend NSDecimalNumber
to conform to Encodable
& Decodable
protocols. Jordan Rose explains it in the following swift evolution email thread.
If you need NSDecimalValue
type in your API you can build computed property around Decimal
.
struct YourType: Codable {
var decimalNumber: NSDecimalNumber {
get { return NSDecimalNumber(decimal: decimalValue) }
set { decimalValue = newValue.decimalValue }
}
private var decimalValue: Decimal
}
Btw. If you are using NSNumberFormatter
for parsing, beware of a known bug that causes precision loss in some cases.
let f = NumberFormatter()
f.generatesDecimalNumbers = true
f.locale = Locale(identifier: "en_US_POSIX")
let z = f.number(from: "8.3")!
// z.decimalValue._exponent is not -1
// z.decimalValue._mantissa is not (83, 0, 0, 0, 0, 0, 0, 0)
Parse strings this way instead:
NSDecimalNumber(string: "8.3", locale: Locale(identifier: "en_US_POSIX"))
How would I decode a NSDecimalNumber without loss of precision?
The issue is that JSONDecoder
is just a wrapper around JSONSerialization
, which decodes decimal numbers into Double
internally and only after converts them to Decimal
. Sadly, unless you create your own JSON decoder, you cannot get around this issue when using numeric decimals from JSON.
The only workaround currently possible is to change your backend to send all decimal numbers as String
s and then convert those into Decimal
s after decoding them as String
s.
For more information, have a look at this open Swift bug: SR-7054.
Swift: Decode imprecise decimal correctly
You can implement your own decoding method, convert your double to string and use it to initialize your decimal properties:
extension LosslessStringConvertible {
var string: String { .init(self) }
}
extension FloatingPoint where Self: LosslessStringConvertible {
var decimal: Decimal? { Decimal(string: string) }
}
struct Root: Codable {
let priceAfterTax, priceBeforeTax, tax, taxAmount: Decimal
}
extension Root {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.priceAfterTax = try container.decode(Double.self, forKey: .priceAfterTax).decimal ?? .zero
self.priceBeforeTax = try container.decode(Double.self, forKey: .priceBeforeTax).decimal ?? .zero
self.tax = try container.decode(Double.self, forKey: .tax).decimal ?? .zero
self.taxAmount = try container.decode(Double.self, forKey: .taxAmount).decimal ?? .zero
}
}
let data = Data("""
{
"priceAfterTax": 150.00,
"priceBeforeTax": 130.43,
"tax": 15.00,
"taxAmount": 19.57
}
""".utf8)
let decodedObj = try! JSONDecoder().decode(Root.self, from: data)
decodedObj.priceAfterTax // 150.00
decodedObj.priceBeforeTax // 130.43
decodedObj.tax // 15.00
decodedObj.taxAmount // 19.57
How to store Big Integer in Swift 4 Codable?
Use built-in Decimal which derives from NSDecimalNumber. It adopts Codable
Obtaining an NSDecimalNumber from a locale specific string?
Based on Boaz Stuller's answer, I logged a bug to Apple for this issue. Until that is resolved, here are the workarounds I've decided upon as being the best approach to take. These workarounds simply rely upon rounding the decimal number to the appropriate precision, which is a simple approach that can supplement your existing code (rather than switching from formatters to scanners).
General Numbers
Essentially, I'm just rounding the number based on rules that make sense for my situation. So, YMMV depending on the precision you support.
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[formatter setGeneratesDecimalNumbers:TRUE];
NSString *s = @"0.07";
// Create your desired rounding behavior that is appropriate for your situation
NSDecimalNumberHandler *roundingBehavior = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundPlain scale:2 raiseOnExactness:FALSE raiseOnOverflow:TRUE raiseOnUnderflow:TRUE raiseOnDivideByZero:TRUE];
NSDecimalNumber *decimalNumber = [formatter numberFromString:s];
NSDecimalNumber *roundedDecimalNumber = [decimalNumber decimalNumberByRoundingAccordingToBehavior:roundingBehavior];
NSLog([decimalNumber stringValue]); // prints 0.07000000000000001
NSLog([roundedDecimalNumber stringValue]); // prints 0.07
Currencies
Handling currencies (which is the actual problem I'm trying to solve) is just a slight variation on handling general numbers. The key is that the scale of the rounding behavior is determined by the maximum fractional digits used by the locale's currency.
NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[currencyFormatter setGeneratesDecimalNumbers:TRUE];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
// Here is the key: use the maximum fractional digits of the currency as the scale
int currencyScale = [currencyFormatter maximumFractionDigits];
NSDecimalNumberHandler *roundingBehavior = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundPlain scale:currencyScale raiseOnExactness:FALSE raiseOnOverflow:TRUE raiseOnUnderflow:TRUE raiseOnDivideByZero:TRUE];
// image s is some locale specific currency string (eg, $0.07 or €0.07)
NSDecimalNumber *decimalNumber = (NSDecimalNumber*)[currencyFormatter numberFromString:s];
NSDecimalNumber *roundedDecimalNumber = [decimalNumber decimalNumberByRoundingAccordingToBehavior:roundingBehavior];
NSLog([decimalNumber stringValue]); // prints 0.07000000000000001
NSLog([roundedDecimalNumber stringValue]); // prints 0.07
Related Topics
Get Rawvalue from Enum in a Generic Function
Performseguewithidentifier in Swift
How to Zip More Than 4 Publishers
Uisearchcontroller in Navigationitem iOS 11 Apple Way
Swiftui: Detect Finger Position on MAC Trackpad
Forcing Nspersistentcontainer Change Core Data
Realitykit - Set Text Programmatically of an Entity of Reality Composer
One-Way Platform Collisions in Sprite Kit
How to Highlight a Uitextview's Text Line by Line in Swift
How to Override Layerclass in Swift
Changing Tab Bar Font in Swift
Accessing Multiple Audio Hardware Outputs/Channels Using Avfoundation and Swift
Swift Subtle Difference Between Curried and Higher Order Function
Convert Int to Uint32 in Swift
An Nsmanagedobject of Class 'Classname' Must Have a Valid Nsentitydescription.' Error