Developing Multi-Currency Support for iOS

Managing currencies in any language is not for the faint of heart. If you’ve been down this dark path, I’m sure you are familiar with the dearth of good recommendations for how to deal with this. Given our focus on group travel, managing different currencies is part of our nuts and bolts.

What follows is an iOS development blow-by-blow for multi-currency support. If you are a fellow developer, please enjoy! To celebrate our second geeky technical article, we’re kicking-off a new article category, “Dev Blog.”

Making calculations with currencies

For all currency calculations, you should use NSDecimalNumber. It’s a pain, I know, but it avoids any issues you might have with trailing floating point garbage. It also lets you do rounding correctly with relatively little effort. For example, what is US$200.00 divided 3 ways? US$66.67. This doesn’t quite work because you wind up a penny above the original amount, but the rounding works according to US standards.

What happens if you divide CHF200.00 3 ways (CHF is Swiss francs)? CHF66.65. Add this up again and you get CHF199.95. Why do we wind up 5 rappen (pennies) short? wouldn’t 2 fewer be more correct? Nope. The smallest coin currently issued is worth 5 rappen, so rounding “more accurately” is incorrect. The Hungarian Forint doesn’t have pennies, so the same division rounds to HUF67, a whole Forint over the original HUF200.

There are lots of odd cases like this – there are 4 commonly used types of rounding, none of which give 66.66 as a result, which is the most useful outcome of this calculation for Splitwise. When working with currencies, you often need to make decisions yourself about where the loose change winds up. This is how we divide some currency value:

- (NSDictionary *)divideEvenlyWithRemainder:(NSDecimalNumber *)numerator
                                         by:(NSDecimalNumber *)denominator
                                forCurrency:(NSString *)currencyCode {
    int32_t precision;
    double rounding;
    CFNumberFormatterGetDecimalInfoForCurrencyCode((__bridge CFStringRef)currencyCode, &precision, &rounding);
    NSDecimalNumberHandler *roundDown = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundDown
                                                                                               scale:precision
                                                                                    raiseOnExactness:NO
                                                                                     raiseOnOverflow:NO
                                                                                    raiseOnUnderflow:NO
                                                                                 raiseOnDivideByZero:NO];

    NSDecimalNumber *roundDecimal = nil;
    NSDecimalNumber *result = nil;
    //If we have rounding, we need to convert to a count of the smallest physical currency
    if (rounding > 0) {
        //As far as I am aware, the smallest rounding is 0.005, added one more sig fig for safety
        roundDecimal = [NSDecimalNumber decimalNumberWithMantissa:rounding * 10000
                                                         exponent:-4
                                                       isNegative:NO];
        //Because we are now working with whole units, we set scale to 0
        roundDown = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundDown
                                                                           scale:0
                                                                raiseOnExactness:NO
                                                                 raiseOnOverflow:NO
                                                                raiseOnUnderflow:NO
                                                             raiseOnDivideByZero:NO];

        //this will give us a count of the smallest available physical currency for countries with
        //coins that are larger or smaller than a "penny"
        result = [numerator decimalNumberByDividingBy:roundDecimal withBehavior:roundDown];
        result = [result decimalNumberByDividingBy:denominator
                                      withBehavior:roundDown];
        //Convert back to currency value instead of count
        result = [result decimalNumberByMultiplyingBy:roundDecimal];
    } else {
        result = [numerator decimalNumberByDividingBy:denominator
                                         withBehavior:roundDown];
    }

    //Have to use original here in case the user decides to put in units smaller than all4owed for some reason
    NSDecimalNumber *remainder = [numerator decimalNumberBySubtracting:[result decimalNumberByMultiplyingBy:denominator]];

    return [NSDictionary dictionaryWithObjectsAndKeys:
            result, @"amount",
            remainder, @"remainder",
            nil];

}

We force rounding down to make sure our total winds up under the original value. We also take into account the rounding increment for the currency, which takes into account issues like Swiss Francs not having pennies. There’s not much error checking here because our use cases come pre-validated, but if you use this for general currency calculations, I’d advise adding error handlers to the rounding behaviors to deal with the edge cases. I’ve also not checked any of this for negative values.

I still need to check and see if any of the currencies we handle have a minimum bill size (for example, if $5 was the smallest US bill rounding to $1 would be wrong). If anyone is aware of any of our currencies that have this issue, please leave me a comment.

Formatting currency output

I’m still working through tests to make sure this is the case, but it appears that the only pieces of locale information that are used to determine currency display are the language and currency settings. If you just set the currency setting on the locale, the formatter will use the international version of the currency symbol, which is typically the currency code or the country code plus the local currency symbol. If you also set the language, the formatter will use the symbol most appropriate for that language and put the currency symbol in the correct position. Here are a couple of correct formats for 200 US dollars:
$200.00
US$ 200,00
200,00 US$
200,00 $

Formatting output is pretty easy, you just create a NSNumberFormatter with the correct behaviors and locale:

NSString *currencyCode = @"USD";
NSDictionary *localeInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                                            currencyCode, NSLocaleCurrencyCode,
                                            [[NSLocale preferredLanguages] objectAtIndex:0], NSLocaleLanguageCode,
                                            nil];

NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:
            [NSLocale localeIdentifierFromComponents:localeInfo]];

NSNumberFormatter *fmt = [[NSNumberFormatter alloc] init];
[fmt setNumberStyle:NSNumberFormatterCurrencyStyle];
[fmt setLocale:locale];

NSLog(@"an example: %@", [fmt stringFromNumber:[NSNumber numberWithFloat:27.53]]);

Handling currency input

This one is also pretty tricky, and I’m planning to avoid it through UI design rather than code. If you just need a quick and dirty input handler, call setLenient:YES on the formatter and wrap calls to numberFromString: in a try/catch. If you know a good and correct solution to handling input for many different currencies, please point me to it in a comment! I’ve seen many incomplete solutions while looking for answers on the internet, and the various solutions I’ve seen don’t handle some of the following: decimal separators, group separators, Chinese counting characters, internationalized or localized currency symbols, currency symbols appearing after the amount (see fr formats), unicode characters or garbage input. It’s not a simple problem and NSNumberFormatter does not deal with input nearly as cleanly as it does with output.

4 thoughts on “Developing Multi-Currency Support for iOS”

  1. Do you want unlimited articles for your blog ?
    I’m sure you spend a lot of time writing articles,
    but you can save it for other tasks, just search
    in google: kelombur’s favorite tool

  2. > If you know a good and correct solution to handling input for many different currencies
    I’ve created custom class for UITextField which has a row number value aside from the text. I registered an observer to track textField value chages and output the internal value acording to currency selected (inputViuw is also custom with Currency Select and NumPad).

      1. We did something similar to this internally. The issues that were tricky to deal with were correctly parsing , and . in different languages and also dealing with CHF (only goes down to 5cents, not 1cent), HUF (whole number values only) and there’s some south american currency that uses half pennies. We don’t solve these completely correctly, but we do pretty well.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s