Ad

Our DNA is written in Swift
Jump

iOS 5 Breaking NSDateFormatter?

Robert Meraner asks:

“Do you have any idea why this code works in iOS 4.3, but no longer under iOS 5? Googling it seems to turn up some ideas, but no immediate explanation.”

This is the code Robert refers to:

NSString *currentElementValue = @"01.12.2011 09:35:13 CET";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"dd.MM.yyyy HH:mm:ss zzz"];
NSDate *date = [dateFormatter dateFromString:currentElementValue];

This got me confounded initially as well, but thanks to Cédric Luthi we got an official answer to this riddle: works as intended!

Trying out the above mentioned code you indeed find that date is correctly parsed on the 4.3 iPhone Simulator. If you simulate it on the 5.0 iPhone Simulator then you get a mysterious nil.

NSDateFormatter has an NSLocale property to specify how text is output or – if you use it for parsing – is to be interpreted. So my first instinct was to try different locales because of the CET time zone abbreviation.

Lo and behold, using en_GB did the trick.

NSLocale *enLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en-GB"];
dateFormatter.locale = enLocale;
[enLocale release];

I also checked which locale was used if you didn’t set the property and found that it defaulted to en-US. So apparently there was a change for this locale in iOS 5.

Cédric had exactly the same problem and raised it with Apple. He was so kind as to share his Radar as well as the detailed response he got from an Apple engineer (emphasis mine):

Engineering has determined that this issue behaves as intended based on the following information:

This is an intentional change in iOS 5. The issue is this: With the short formats as specified by z (=zzz) or v (=vvv), there can be a lot of ambiguity. For example, “ET” for Eastern Time” could apply to different time zones in many different regions. To improve formatting and parsing reliability, the short forms are only used in a locale if the “cu” (commonly used) flag is set for the locale. Otherwise, only the long forms are used (for both formatting and parsing). This is a change in open-source CLDR 2.0 / ICU 4.8, which is the basis for the ICU in iOS 5, which in turn is the basis of NSDateFormatter behavior.

For the “en” locale (= “en_US”), the cu flag is set for metazones such as Alaska, America_Central, America_Eastern, America_Mountain, America_Pacific, Atlantic, Hawaii_Aleutian, and GMT. It is not set for Europe_Central.

However, for the “en_GB” locale, the cu flag is set for Europe_Central.

So, a formatter set for short timezone style “z” or “zzz” and locale “en” or “en_US” will not parse “CEST” or “CET”, but if the locale is instead set to “en_GB” it will parse those. The “GMT” style will be parsed by all.

If the formatter is set for the long timezone style “zzzz”, and the locale is any of “en”, “en_US”, or “en_GB”, then any of the following will be parsed, because they are unambiguous:

“Pacific Daylight Time” “Central European Summer Time” “Central European Time”

The mentioned ICU change (International Components for Unicode) is quite a mouthful, so we can be thankful that Apple is on top of what these Open Source guys are deciding and their engineers are merging all these locale fixes into our favorite operating system.

In this special case we learn that it is not wise to use time zone abbreviations in server-generated XML. Better to have all dates use UTC with time zone “Z”. Alternatively you could use the UTC offset. This is never ambiguous.

If you have no say in what comes from the server then the officially recommended locale that understands CET and CEST would be en_GB. You should always use the same locale to decode a date string as was used to encode it. The necessity for this would also be present if month names are spelled out or abbreviated in the source string.

In short: Thou shalt not rely on the default locale to understand your date strings.


Categories: Q&A

5 Comments »

  1. Interesting, we stumbled across that and solved it by following the NSDateFormatter documentation/examples – now here is the explanation, thanks!

  2. Ah if only you had asked. Apple are always changing date formatters one way or another between version. Now if only you had a crappy alarm click app in the app store 🙂

  3. I’m glad to see that this is a known problem, because it took me a while to understand that, thinking I was doing things wrong.

    Nevertheless, I am too stupid to understand what’s going on here.

    Could anyone tell me how to make the following work in iOS5? It used to be working fine:

    NSLocale *myLocale = [[NSLocale alloc] initWithLocaleIdentifier:@”en_GB”];
    [inputFormatter setLocale: myLocale];
    [myLocale release];
    [inputFormatter setDateFormat:@”EEE, dd MMM yyyy HH:mm:ss Z”];
    NSDate *formattedDate = [inputFormatter dateFromString: feedDateString];
    NSDateFormatter *outputFormatter = [[NSDateFormatter alloc] init];
    [outputFormatter setDateFormat:@”EEEE d MMMM”];

    The input string, that is beyond my control, would be something like “Tue, 03 Jan 2012 00:00:00 +0100”
    When I log [outputFormatter stringFromDate:formattedDate] I get null

    The original Locale was “en_US”, but I changed that, because I thought I understood a bit of the post here. So not.

    Your help is appreciated.

  4. Hey — I’ve been fighting with this problem for hours to no avail.

    NSDateFormatter* df = [[NSDateFormatter alloc]init];

    [df setDateFormat:@”yyyy-MM-d HH:mm:ss z”];

    NSLocale *enLocale = [[NSLocale alloc] initWithLocaleIdentifier:@”en-US”];
    df.locale = enLocale;

    // [enLocale release];

    NSDate *updatedDate = [df dateFromString:[objectDictionary objectForKey:@”updated”]];

    // 2012-01-17 13:31:08 ET

    if (updatedDate == nil)
    [NSException raise:@”WRONG” format:@”NSDate from dateFormatter was still nil”];

  5. @sheldon

    I just fought for hours too on a Mac program…. Try this, it is a transposition of my solution :

    NSDateFormatter* df = [[NSDateFormatter alloc]init];
    [df setDateFormat:@”yyyy-MM-dd HH:mm:ss zzz”];
    [df setLocale:enUS];
    [df setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];

    YMMV…