Obtaining an NSDecimalNumber from a locale specific string?
I have some string s that is locale specific (eg, 0.01 or 0,01). I want to convert this string to a NSDecimalNumber. From the examples I’ve seen thus far on the interwebs, this is accomplished by using an NSNumberFormatter a la:
NSString *s = @"0.07"; NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; [formatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [formatter setGeneratesDecimalNumbers:YES]; NSDecimalNumber *decimalNumber = [formatter numberFromString:s]; NSLog([decimalNumber stringValue]); // prints 0.07000000000000001
I’m using 10.4 mode (in addition to being recommended per the documentation, it is also the only mode available on the iPhone) but indicating to the formatter that I want to generate decimal numbers. Note that I’ve simplified my example (I’m actually dealing with currency strings). However, I’m obviously doing something wrong for it to return a value that illustrates the imprecision of floating point numbers.
What is the correct method to convert a locale specific number string to an NSDecimalNumber?
Edit: Note that my example is for simplicity. The question I’m asking also should relate to when you need to take a locale specific currency string and convert it to an NSDecimalNumber. Additionally, this can be expanded to a locale specific percentage string and convert it to a NSDecimalNumber.
- How to get the path to the current workspace/screen's wallpaper on OSX?
- OSX status menu not working in Swift
- What is an easy way to break an NSArray with 4000+ objects in it into multiple arrays with 30 objects each?
- NSString encoding special characters like !@#$%^&
- NSAttributedString initWithData and NSHTMLTextDocumentType crash if not on main thread
- Use Quick Look inside a Swift cocoa application to preview audio files
4 Solutions Collect From Internet About “Obtaining an NSDecimalNumber from a locale specific string?”
+(NSDecimalNumber *)decimalNumberWithString:(NSString *)numericString in
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).
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
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
This seems to work:
NSString *s = @"0.07"; NSScanner* scanner = [NSScanner localizedScannerWithString:s]; NSDecimal decimal; [scanner scanDecimal:&decimal]; NSDecimalNumber *decimalNumber = [NSDecimalNumber decimalNumberWithDecimal:decimal]; NSLog([decimalNumber stringValue]); // prints 0.07
Also, file a bug on this. That’s definitely not the correct behavior you’re seeing there.
Edit: Until Apple fixes this (and then every potential user updates to the fixed OSX version), you’re probably going to have to roll your own parser using NSScanner or accept ‘only’ double accuracy for entered numbers. Unless you’re planning to have the Pentagon budget in this app, I’d suggest the latter. Realistically, doubles are accurate to 14 decimal places, so at anything less than a trillion dollars, they’ll be less than a penny off. I had to write my own date parsing routines based on NSDateFormatter for a project and I spent literally a month handling all the funny edge cases, (like how only Sweden has the day of week included in its long date).
See also Best way to store currency values in C++
The best way to handle currency is to use an integer value for the smallest unit of the currency, i.e. cents for dollars/euros, etc. You’ll avoid any floating point related precision errors in your code.
With that in mind, the best way to parse strings containing a currency value is to do it manually (with a configurable decimal point character). Split the string at the decimal point, and parse both the first and second part as integer values. Then use construct your combined value from those.
- How to loop through an array of objects in swift
- iOS8 Swift: deleteRowsAtIndexPaths crashes
- Xcode Project Resource Organization and Structure
- Drawing Route Between Two Places on GMSMapView in iOS
- Duplicate, clone or copy UIView
- UIButton titleLabel frame size returning CGSize with zero width & height
- How to conform to CBCentralManagerDelegate Protocol?
- Create a TabBar Controller with a Master-detail template?
- Application not in the App Store Search
- What the equivalent of activity life cycle in iOS?
- UISegmentedControl below UINavigationbar in iOS 7
- Objective C – How do I use initWithCoder method?
- How to mask the layer of a view by the content of another view?
- UIPanGestureRecognizer isn't firing up. Swift 3
- Wait For Asynchronous Operation To Complete in Swift