International Date Formatting Gotchas

Any iOS developer can tell you that you should always be formatting dates with an NSDateFormatter object (or the underlying C equivalent strptime, if you’re extremely concerned about speed). In Tappd That, I constantly need to parse dates that come across the wire in the format “Thu, 09 Jan 2014 12:03:00 +0000”. This is trivial using an NSDateFormatter, since I can just refer to the Unicode Technical Standard #35, revision 25 to set a dateFormat of @"EEE, dd MMM yyyy HH:mm:ss Z". Perfect, right?

Wrong. So wrong. This works as you’d expect it to 90% of the time, but fails miserably elsewhere…that is, if you don’t set a locale for the format of the string to be parsed. In Tappd That’s case, strings will always be retrieved from a web service serving US English style dates (ignoring HTTP’s Accept-Language header). Many countries use the English abbreviations for months and days of the week despite the Language setting on the phone. It’s the Region Format that you care about. Consider the following:

static NSDateFormatter *dateFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	dateFormatter = [NSDateFormatter new];
	dateFormatter.formatterBehavior = NSDateFormatterBehavior10_4;
	dateFormatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss Z";
});
 
NSDate *date = [dateFormatter dateForString:@"Thu, 09 Jan 2014 12:03:00 +0000"];
NSLog(@"%@", [NSDateFormatter localizedStringFromDate:date
                                            dateStyle:NSDateFormatterShortStyle
                                            timeStyle:NSDateFormatterShortStyle]);

With the Region Format set to “United States” (selected via Settings > General > International > Region Format), the code above prints “1/9/14, 7:03 AM“. “France” prints “09/01/14 07:03,” and now you think you’re an Internationalization expert since your code made the day come first. I sure did. Some random format you’ve never heard of? Comes out like the US. But what about “Danish (Denmark)?” (null). Yeah, didn’t see that one coming, did you? 

As I explained, the problem is that the NSDateFormatter is expecting the regional equivalents for the month and day of the week, even though the Language setting is English. The solution here is to add a locale. Since I know my dates coming from the web service will always be in English, I can set the locale as follows (or use strptime_l with NULL for the en_US_POSIX default):

dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];

It’s somewhat important to use the en_US_POSIX locale instead of just en_US. While they’ll both work currently (2014), if the United States ever decided to change their date format, en_US would return nil again, while en_US_POSIX would continue to work. Apple has a Technical Q&A about this topic that should be required reading for anyone formatting dates internationally. So why did the random country work? That’s in the documentation: “Note that many of the country codes do not have any supporting locale data in iOS.”

But wait, there’s more! You also need to account for alternate Calendars! Not all of us are in the year 2014 AD. The Buddhists are currently living in 2556 BE. In my case, I know that dates will always be coming from an English Gregorian calendar and the timezone is GMT (I parse that above with Z, but it’s always +0000. You think they’d just omit it.).

NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
gregorianCalendar.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
gregorianCalendar.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
dateFormatter.calendar = gregorianCalendar;

While you’re thinking internationally, you also want to make sure that you’re using the currentLocale of the user’s device when printing dates to make sure they’re printed natively (this is the default for new NSDateFormatters, as you’ve discovered from my troubles above). This way, your Danish (Denmark) users can see “torsdag den 9. januar 2014 07.03.00 Eastern-normaltid” instead of “(null).”

Leave a Reply

Your email address will not be published. Required fields are marked *