Obtaining an NSDecimalNumber from a locale specific string? - iphone

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.

Years later:
+(NSDecimalNumber *)decimalNumberWithString:(NSString *)numericString in NSDecimalNumber.

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

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.

Related

Convert a numeric value in a string to integer in Objective C/C

I have a NSString with me. This NSString is obtained from a Voice engine. The voice input is converted to native NSString.
See the following string:
"Set heat to thirty two degree"
Is there any way to get this converted to
"Set heat to 32 degree"
If there are some third party library that does this conversion, it would be really helpful. Otherwise I will have to create a complex logic to get this done it seems.
And that's where a forgotten NSNumberFormatter can show what it's capable of doing.
After you parsed your input string (using regular expression magic or NSString's componentsSeparatedByString:, I'll not be discussing that step) and obtained the spell-out number, you can use NSNumberFormatter's NSNumberFormatterSpellOutStyle to quickly convert your string into a number:
NSString *obtainedDegreesString = #"thirty-two";
NSNumberFormatter *spellOutFormatter = [[NSNumberFormatter alloc] init];
[spellOutFormatter setLocale:[NSLocale currentLocale]]; // or whatever locale you want
[spellOutFormatter setNumberStyle:NSNumberFormatterSpellOutStyle];
NSNumber *degreesNumber = [spellOutFormatter numberFromString:obtainedDegreesString];
NSLog(#"%d", degreesNumber.intValue); // logs 32
But warning - you have to convert strings like "thirty two" to "thirty-two" (the correct English numerals) for your formatter to work - passing "thirty two" results in 3002! Your users probably don't want to burn in 3002 degrees :P You can achieve that using another regular expression, I suppose.

Trying to save long long into NSNumber from String

I am trying to save a long long number (received as a string) such as '80182916772147201' into an NSNumber.
NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
[f setNumberStyle:NSNumberFormatterBehaviorDefault];
[item setObject:[f numberFromString:#"80182916772147201"] forKey:#"theID"];
[f release];
When I NSLog this out, assuming the string was '80182916772147201' I get:
NSLog(#"%lld", [[item objectForKey:#"theID"] longLongValue]);
Returns: '80182916772147200' - Note the rounded down final digit.
What am I doing wrong?
The problem is that NSNumberFormatter has decided to represent that number as a floating-point number. To force it to use integers only:
[f setAllowsFloats:NO];
Can you try this?
NSString *numStr = [NSString stringWithFormat:#"%llu", [myNum unsignedLongLongValue]];
This makes a few reasonable assumptions such as numStr will only contain numeric digits and it contains a 'valid' unsigned long long value. A drawback to this approach is that UTF8String creates what essentially amounts to [[numStr dataUsingEncoding:NSUTF8StringEncoding] bytes], or in other words something along the lines of 32 bytes of autoreleased memory per call. For the vast majority of uses, this is no problem what-so-ever.
For an example of how to add something like unsignedLongLongValue to NSString that is both very fast and uses no autoreleased memory as a side effect, take a look at the end of my (long) answer to this SO question. Specifically the example implementation of rklIntValue, which would require only trivial modifications to implement unsignedLongLongValue.

iPhone -> NSNumberFormatter - price without the currency code

I have an iPhone app and I want to display a price. So I use these lines of code:
NSNumberFormatter *price = [[NSNumberFormatter alloc] init];
[price setNumberStyle:NSNumberFormatterCurrencyStyle];
[price setCurrencyCode:#"USD"];
What I want is only the price, formatted for the currencyCode but in my case without USD.
So instead of 30,00 $, I want to have only 30,00 without $.
How can I do this?
Best Regards, Tim.
What you're looking to get rid of is the Currency Symbol.
[price setCurrencySymbol:#""];
You can use the setCurrencySymbol to an empty string which should do the trick:
[price setCurrencySymbol:#""];
Please keep in mind that this only sets the local currency code and it might not work with foreign currencies.
Here's a way to do it while keeping the correct locale formatting for the symbol:
_fmt.positiveFormat = [_fmt.positiveFormat
stringByReplacingOccurrencesOfString:#"¤" withString:#""];
_fmt.negativeFormat = [_fmt.negativeFormat
stringByReplacingOccurrencesOfString:#"¤" withString:#""];
The ¤ character is used in the currency formats for the currency symbol. If you replace the currency symbol as suggested in some of the other answers, the formatter does a US standard format for the number (AFAIK - tested in iOS 4 and 5 in the USA). You'll need to do this replacement after setting the locale for the formatter. Try with the currency code HUF which does not have pennies.

Bug with iPhone Currency for Euro format with NSDecimalNumber using NSLocale?

My app uses monetary values, and I would like to represent these values in the currency and formats that the user has set on their iPhone however, when testing, I seem to be having troubles with the German EURO format.
I have a method that returns a decimal number (for storing in core data) from the currency formatted string. The string was actually formatted by another (similar) method using NSlocale.
I have tested with UK, US, Japanese and German locales, so far only Germany has caused any problems.
Here's the code, the string being passed is for example: 2,56 €
+ (NSDecimalNumber*)TLCurrencyFormatFromString:(NSString*)strNumber {
NSNumberFormatter *fmtrCurrencyFromStr = nil;
fmtrCurrencyFromStr = [[NSNumberFormatter alloc] init];
[fmtrCurrencyFromStr setFormatterBehavior:NSNumberFormatterBehavior10_4];
[fmtrCurrencyFromStr setLocale:[NSLocale currentLocale]];
[fmtrCurrencyFromStr setNumberStyle:NSNumberFormatterCurrencyStyle];
[fmtrCurrencyFromStr setGeneratesDecimalNumbers:YES];
NSLog(#"Currency Separator: %#", [fmtrCurrencyFromStr decimalSeparator]); //prints a comma
NSLog(#"Currency Code: %#", [fmtrCurrencyFromStr currencyCode]); //prints EUR
int currencyScale = [fmtrCurrencyFromStr maximumFractionDigits];
NSDecimalNumberHandler *roundingBehavior = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundPlain scale:currencyScale raiseOnExactness:FALSE raiseOnOverflow:TRUE raiseOnUnderflow:TRUE raiseOnDivideByZero:TRUE];
NSDecimalNumber* currencyNumber = [[NSDecimalNumber alloc] initWithDecimal:[[fmtrCurrencyFromStr numberFromString:strNumber] decimalValue]];
NSLog(#"Currency Number = %#", [currencyNumber decimalNumberByRoundingAccordingToBehavior:roundingBehavior]);
return [currencyNumber decimalNumberByRoundingAccordingToBehavior:roundingBehavior];
}
The 'currencyNumber' variable gets printed as: -18446912453607470532000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
whereas in all the other locales, it would print 2.56 for storing into Core Data as NSDecimalNumber.
Setting the property:
[fmtrCurrencyFromStr setLenient:YES];
Fixes the problem for France, Germany, Norway, UK, US, Japan, Nambia etc... I can't say it fixes it for all because I haven't tested all, but of all those that I've tested so far, it has worked.
I think your problem is with the line
NSDecimalNumber* currencyNumber = [[NSDecimalNumber alloc] initWithDecimal:[[fmtrCurrencyFromStr numberFromString:strNumber] decimalValue]];
Try replacing it with the mostly-equivalent line
NSDecimalNumber * currencyNumber = [fmtrCurrencyFromStr numberFromString:strNumber];
You've set generatesDecimalNumbers, so the two are pretty much equivalent except for one annoying case: nil.
Sending a message to nil is guaranteed to do nothing and return 0 for most standard types (integer (except not 64-bit integers on old 32-bit runtimes), floating point, pointer types); it "returns" uninitialized memory if the method returns a struct. NSDecimal is a struct.
(The underlying reason is essentially that the runtime doesn't know how big the struct is, so doesn't know how much memory to zero, hence you get "uninitialized" memory instead. I usually hit this one when I do v.frame without ensuring that v is not nil.)
EDIT: Of course, you other problem is why NSNumberFormatter isn't parsing the price correctly. That's a tough one. I'd try stripping the currency symbol.
I verified that on iOS 4.3 using the [formatter setLenient:YES] flag worked for me as well using Euros. Without that flag set, the formatter does not return correct values for Euros.

How to display currency without rounding as string in Xcode?

I have trouble when I have currency value
999999999999999999.99
From that value, I want to display it as String. The problem is that value always rounded to
1000000000000000000.00
I'm not expect that value rounded. I want the original value displayed. Do you have any idea how to solve this problem? I tried this code from some answer/turorial in stackoverflow.com :
NSMutableString *aString = [NSMutableString stringWithCapacity:30];
NSNumberFormatter *aCurrency = [[NSNumberFormatter alloc]init];
[aCurrency setFormatterBehavior:NSNumberFormatterBehavior10_4];
[aCurrency setNumberStyle:NSNumberFormatterCurrencyStyle];
[aCurrency setMinimumFractionDigits:2];
[aCurrency setMaximumFractionDigits:2];
[aString appendString:[aCurrency stringFromNumber:productPrice]];
//productPrice is NSDecimalNumber which is have value 999999999999999999.99
cell.textLabel.text = aString;
NSLog(#"the price = %#",cell.textLabel.text);
//it prints $1,000,000,000,000,000,000.00
[aCurrency release];
Unless you have very good reasons not to, it is generally best to keep currency values in a fixed-point format, rather than floating point. Ada supports this directly, but for C-ish languages what you do is keep the value in units of pennies, rather than dollars, and only do the conversion whenever you go to display it.
So in this case the value of productPrice would be 99999999999999999999 (cents). To display it, you'd do something like this (If this were C. I don't know the language):
int const modulus = productPrice % 100;
printf ("The price = %d.%d\n", (int) ((productPrice - modulus) / 100), modulus);
I'd also use an integer rather than a floating point variable to keep the value in almost all cases. It won't work in this case (even if you use a 64-bit integer) because your value is mind-bogglingly large. We're talking 1 million times larger than the US National Debt! If a dollar value that large ever makes sense for anything in my lifetime, we are all in big trouble.
Have you tried these:
* – setRoundingBehavior:
* – roundingBehavior
* – setRoundingIncrement:
* – roundingIncrement
* – setRoundingMode:
* – roundingMode