0

I have a UITextField that receives numeric input from the user in my application. The values from this textfield then get converted into currency format using NSNumberFormatter within my shouldChangeCharactersInRange delegate method. When I enter the number "12345678", the number gets correctly converted to $123456.78 (the numbers are entered one digit at a time, and up to this point, everything works smoothly). However, when I enter another digit after this (e.g. 9), rather than displaying "1234567.89", the number "1234567.88" is displayed. If I enter another number after that, a totally different numbers after this (I'm using the number key pad in the application to enter the numbers. Here is the code that I have:

NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setNumberStyle:NSNumberFormatterCurrencyStyle];
modifiedValue = [formatter stringFromNumber:[NSNumber numberWithFloat:[modifiedValue floatValue]]];
textField.text = modifiedValue;

The line that causes this unusual conversion is this one:

modifiedValue = [formatter stringFromNumber:[NSNumber numberWithFloat:[modifiedValue floatValue]]];

Can anyone see why this is?

iDev
  • 23,310
  • 7
  • 60
  • 85
syedfa
  • 2,801
  • 1
  • 41
  • 74
  • 1
    You should show all of your `shouldChangeCharactersInRange` method. Besides the fact that `float` has a limited number of significant digits (use `double` instead), you may have other issues as well. – rmaddy Mar 06 '13 at 23:10

2 Answers2

5

It's likely to be a rounding error when doing the string->float conversion. You shouldn't use floats when dealing with currency. You could use a NSDecimalNumber instead.

NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setNumberStyle:NSNumberFormatterCurrencyStyle];
// Below 2 lines if converting from a "currency" string
NSNumber *modifiedNumber = [formatter numberFromString:modifiedValue]; // To convert from the currency string to a number object
NSDecimalNumber *decimal = [NSDecimalNumber decimalNumberWithDecimal:[modifiedNumber decimalValue]]; 
// OR the below line if converting from a non-currency string 
NSDecimalNumber *decimal = [NSDecimalNumber decimalNumberWithString:modifiedValue];
modifiedValue = [formatter stringFromNumber:decimal]; // Convert the new decimal back to a currency string

You may also consider making the number formatter lenient - often helps with user entered data.

[formatter setLenient:YES];
rickerbh
  • 9,731
  • 1
  • 31
  • 35
  • 1
    @syedfa It applies a level of heuristics to the conversion, to account for other characters users may (or may not) type in. With lenient = NO, 1234567.89 converts to $0.00, and $1234567.89 converts to $1,234,567.89. With lenient = YES, both 1234567.89 and $1234567.89 convert to $1,234,567.89. – rickerbh Mar 06 '13 at 23:32
  • Believe it or not, your previous answer works better than this updated one by :-) Not sure why, but its fewer lines, and it consistently receives the input correctly. – syedfa Mar 06 '13 at 23:37
  • Here is the original code block that you posted: NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; [formatter setNumberStyle:NSNumberFormatterCurrencyStyle]; NSDecimalNumber *decimal = [NSDecimalNumber decimalNumberWithString:modifiedValue]; modifiedValue = [formatter stringFromNumber:decimal]; textField.text = modifiedValue; – syedfa Mar 06 '13 at 23:37
  • Yeah, I didn't know if modifiedValue in your code was going to include currency formatting or not. If it does, then the original code block won't work correctly. If it doesn't, then the original will be fine. – rickerbh Mar 07 '13 at 00:04
0

When I'm running number conversions to currency, I usually run this code:

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSString *text             = _textField.text;
    NSString *decimalSeperator = @".";
    NSCharacterSet *charSet    = nil;
    NSString *numberChars      = @"0123456789";


    // the number formatter will only be instantiated once ...

    static NSNumberFormatter *numberFormatter;
    if (!numberFormatter)
    {
        [numberFormatter setLocale:[NSLocale currentLocale]];
        numberFormatter = [[NSNumberFormatter alloc] init];
        numberFormatter.numberStyle           = NSNumberFormatterCurrencyStyle;
        numberFormatter.maximumFractionDigits = 10;
        numberFormatter.minimumFractionDigits = 0;
        numberFormatter.decimalSeparator      = decimalSeperator;
        numberFormatter.usesGroupingSeparator = NO;
    }


    // create a character set of valid chars (numbers and optionally a decimal sign) ...

    NSRange decimalRange = [text rangeOfString:decimalSeperator];
    BOOL isDecimalNumber = (decimalRange.location != NSNotFound);
    if (isDecimalNumber)
    {
        charSet = [NSCharacterSet characterSetWithCharactersInString:numberChars];        
    }
    else
    {
        numberChars = [numberChars stringByAppendingString:decimalSeperator];
        charSet = [NSCharacterSet characterSetWithCharactersInString:numberChars];
    }


    // remove amy characters from the string that are not a number or decimal sign ...

    NSCharacterSet *invertedCharSet = [charSet invertedSet];
    NSString *trimmedString = [string stringByTrimmingCharactersInSet:invertedCharSet];
    text = [text stringByReplacingCharactersInRange:range withString:trimmedString];


    // whenever a decimalSeperator is entered, we'll just update the textField.
    // whenever other chars are entered, we'll calculate the new number and update the textField accordingly.

    if ([string isEqualToString:decimalSeperator] == YES) 
    {
        textField.text = text;
    }
    else 
    {
        NSNumber *number = [numberFormatter numberFromString:text];
        if (number == nil) 
        {
            number = [NSNumber numberWithInt:0];   
        }
        textField.text = isDecimalNumber ? text : [numberFormatter stringFromNumber:number];
    }

    return NO; // we return NO because we have manually edited the textField contents.
}

The link explaining this is Re-Apply currency formatting to a UITextField on a change event

Hope this works!

Community
  • 1
  • 1
waylonion
  • 6,866
  • 8
  • 51
  • 92
  • There is a problem with this code. If the user changes the Region Format setting on their iOS device (which changes the current locale), then the number formatter won't be valid anymore and you have no way to reset it even if you listen for locale change notification. – rmaddy Mar 06 '13 at 23:09
  • What if you added this? [formatter setLocale:[NSLocale currentLocale]]; – waylonion Mar 06 '13 at 23:11
  • I used the following block of code that someone posted moments ago: NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; [formatter setNumberStyle:NSNumberFormatterCurrencyStyle]; NSDecimalNumber *decimal = [NSDecimalNumber decimalNumberWithString:modifiedValue]; modifiedValue = [formatter stringFromNumber:decimal]; textField.text = modifiedValue; and it solved my problem, but the irony is that that answer is no longer posted! – syedfa Mar 06 '13 at 23:12
  • @iLive If you are going to do that, do it after you call alloc/init. My preference is to create the formatter as an instance variable or at least a file static. Then I can update the instance or static when I get a locale change notification. – rmaddy Mar 06 '13 at 23:13
  • @syedfa I don't know why that answer was deleted either. Seemed like a good one to me. – rmaddy Mar 06 '13 at 23:13
  • @iLive Also, why do you hard code the number of fraction digits and the decimal separator? This completely ruins the point of using a number formatter based on the user's locale. – rmaddy Mar 06 '13 at 23:16
  • @syedfa I deleted my post as there was a fault with the initial conversion. It's corrected now and undeleted :-) – rickerbh Mar 06 '13 at 23:24