Parsing strings with Objective-C and NSDateFormatter, a gotcha

13 December 2010 Parsing strings with Objective-C and NSDateFormatter, a gotcha

I was recently working on an odd bug in one of my client's iPhone apps; the app in question fetches data from a Rails-based REST API and stores the records in a local Core Data store.

Unfortunately, some users were reporting that some records were simply not appearing. Try as I might, I was unable to reproduce this issue locally.

With some help of one of the users who was having this issue, I was eventually able to track down the core of the problem; the records were fetched from Core Data using a predicate that matched records with a createdAt attribute greater than a particular date however, the records in the database all had nil createdAt dates.

Having located the root cause, the next step was to determine why those dates were nil and the problem lied in the way the dates were being parsed.

The dates returned by the Rails API were in the format '2010-11-28T20:30:49Z' and were in UTC. The method that parsed this string and tried to convert it to an NSDate was implemented as a category on NSDate that looked like this:

@implementation NSDate (StringParsing)
+ (NSDate *)dateWithISO8601String:(NSString *)dateString
{
  if (!dateString) return nil;
  if ([dateString hasSuffix:@"Z"]) {
    dateString = [[dateString substringToIndex:(dateString.length-1)] stringByAppendingString:@"-0000"];
  }
  return [self dateFromString:dateString
                   withFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"];
}
@end

The code first checks that the string is not nil, then replaces the 'Z' at the end of the timestamp with a zero UTC offset string that we can match using NSDateFormatter. Finally, it calls this method:

+ (NSDate *)dateFromString:(NSString *)dateString 
                withFormat:(NSString *)dateFormat 
{
  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  [dateFormatter setDateFormat:dateFormat];
  NSDate *date = [dateFormatter dateFromString:dateString];
  [dateFormatter release];
  return date;
}

This is fairly straightforward; it initialises a new NSDateFormatter, sets the format of the string we are trying to parse and then parses the date.

Ensuring consistent NSDateFormatter behaviour

It's hard to see what could be going wrong with this, especially as I couldn't reproduce the issue. I had already confirmed that the customer with the issue was running the latest version of iOS (as was I) and we were both using the same locale ("en_GB" in this case).

Stumped, I headed to StackOverflow and Twitter, where Ben Dodson pointed me in the right direction.

The solution was simple but not obvious; to ensure that the NSDateFormatter behaviour is consistent across locales and date/time settings, you should explicitly set the appropriate locale of the date formatter which in this case, was "enUSPOSIX". The new method looked like this:

+ (NSDate *)dateFromString:(NSString *)dateString 
                withFormat:(NSString *)dateFormat 
{
  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  [dateFormatter setDateFormat:dateFormat];

  NSLocale *locale = [[NSLocale alloc] 
    initWithLocaleIdentifier:@"en_US_POSIX"];
  [dateFormatter setLocale:locale];
  [locale release];

  NSDate *date = [dateFormatter dateFromString:dateString];
  [dateFormatter release];
  return date;
}

Apple explains this in even more detail in this tech note - if you ever work with dates and timestamp strings from web APIs, I highly recommend you read it.

I spent several hours banging my head against a wall on this one, so hopefully this post will prove useful to somebody else in the future.