6

Update

As of iOS 7, NSDateFormatter does indeed create an NSDate when presented with a string in this format:

NSDateFormatter *formatter = [NSDateFormatter new];
[formatter setDateFormat:@"@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ""];

NSLog(@"non–nil date, even honoring the 7–minute–offset in the time–zone on iOS 7: %@",
     [formatter dateFromString:@"2011-07-12T18:07:31+02:07"]);

For iOS 6, the answer is to not use an NSDateFormatter…


Okay, up to this point I have read

regarding how to use NSDateFormatter in order to create an NSDate out of a string.

I have stumbled upon Peter Hosey's ISO8601DateFormatter, as well.
Looking into his implementation, I wonder:

Isn't there a way that is both correct and sane to get a string like this one 2011-07-12T18:07:31+02:00 into an NSDate?

  • It would be no problem if the last colon was missing.
  • It would be no problem if there was a GMT prefixing the "+"-sign, but...
  • that is not the case.

I can hack it to work for my application (using the format @"yyyy'-'MM'-'dd'T'HH':'mm':'ssz':'00") but that is — of course — incorrect because it will discard the minute-information of the timezone.

I could also replace the last colon with an empty string, but I would consider that a hack as well.

So, is there some secret sauce to make NSDateFormatter take that string from above and give me a valid and correct NSDate?


Aside:

I have somewhere found the tip, that one could use +[NSDate dateWithNaturalLanguageString:] to achieve my goal. This — however — only sets the date, but not the time! (Well it does set the time, but only taking the timezone-offset into account and not the HH:mm:ss part...)

Community
  • 1
  • 1
danyowdee
  • 4,658
  • 2
  • 20
  • 35
  • have you found an actual solution (not a hack) for the ISO8601 offsets? – matm Sep 12 '11 at 16:21
  • @delirus unfortunately, not :-( – danyowdee Sep 13 '11 at 20:49
  • thanks your getting back :) I've found Peter Hosey's `ISO8601DateFormatter` worth considering, but it's reported to be extremely slow (see http://stackoverflow.com/questions/2201216/is-there-a-simple-way-of-converting-an-iso8601-timestamp-to-a-formatted-nsdate) – matm Sep 13 '11 at 21:15

2 Answers2

2

This question is a bit old, but I was having the same problem. I came up with some code which is an answer and might be useful to others...

I use a regex to parse an ISO-8601 string and grab the output into a bunch of strings you can then use to create your own string to pass into NSDateFormatter (i.e. removing colons etc) or if you always are going to want the same output string, just create that from the results of a call to NSRegularExpression.

//    ISO-8601 regex: 
//        YYYY-MM-DDThh:mm[:ss[.nnnnnnn]][{+|-}hh:mm]
// Unfortunately NSDateFormatter does not parse iso-8601 out of the box,
// so we need to use a regex and build up a date string ourselves.
static const char * REGEX_ISO8601_TIMESTAMP = 
            "\\A(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})" // Mandatory - YYYY-MM-DDThh:mm
            "(?:"
            ":(\\d{2})"                                       // Optional - :ss
            "(?:"
            "[.](\\d{1,6})"                                   // Optional - .nnnnnn
            ")?"
            ")?"
            "(?:"
            "([+-])(\\d{2}):(\\d{2})|Z"                       // Optional -[+-]hh:mm or Z
            ")?\\z";

// Extract all the parts of the timestamp
NSError *error = NULL;
NSString *regexString = [[NSString alloc] initWithUTF8String:REGEX_ISO8601_TIMESTAMP];

NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString
                                                                       options:NSRegularExpressionCaseInsensitive
                                                                         error:&error];

NSArray *matches = [regex matchesInString:timestamp
                              options:0
                                range:NSMakeRange(0, [timestamp length])];

// Groups:
//
// elements start at 1 in the array returned from regex, as [0] contains the original string.
//
// MANDATORY - must exist as per ISO standard
//  1  - YYYY
//  2  - MM
//  3  - DD
//  4  - hh
//  5  - mm
// OPTIONAL (each one can be optional)
//  6  - ss
//  7  - nn (microseconds)
//  8  - offset sign (+/-)
//  9  - offset hour
//  10 - offset min
// put the parts into a string which will then be recognised by NSDateFormatter
// (which is acutally RFC822 format)

// mandatory init'd to nil, optional set to defaults.
NSString *YYYY, *MM, *DD, *hh, *mm, *ss, *nn, *sign, *Zhh, *Zmm;
NSRange tempRange;

for (NSTextCheckingResult *match in matches) {
    NSRange matchRange = [match range];
    NSInteger matchCount = [match numberOfRanges] - 1;
    NSUInteger idx = 1;

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        YYYY = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        MM   = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
         tempRange = [match rangeAtIndex:idx++];
         DD   = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        hh   = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        mm   = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        ss   = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        nn = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        sign = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        Zhh  = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }

    if (idx < matchCount) {
        tempRange = [match rangeAtIndex:idx++];
        Zmm  = tempRange.location != NSNotFound ? [timestamp substringWithRange:tempRange] : nil;
    }
}

Hope this helps someone!

Stretch
  • 3,669
  • 2
  • 28
  • 40
  • I totally wasn’t expecting any answers to this one anymore. Seeing the Regexp reminds me of why I didn’t follow that route before ;-) But of course, your answer is correct, so +1 BTW: you can define NSString constants directly by using `NSString * const VARIABLE_NAME = …;` – danyowdee May 07 '12 at 08:53
1

Old question, but I found the right answer on someone's gist :

https://gist.github.com/soffes/840291

It parses and creates ISO-8601 Strings, and it's quicker than NSDateFormatter

Here is the code:

+ (NSDate *)dateFromISO8601String:(NSString *)string {
    if (!string) {
        return nil;
    }

    struct tm tm;
    time_t t;    

    strptime([string cStringUsingEncoding:NSUTF8StringEncoding], "%Y-%m-%dT%H:%M:%S%z", &tm);
    tm.tm_isdst = -1;
    t = mktime(&tm);

    return [NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
}


- (NSString *)ISO8601String {
    struct tm *timeinfo;
    char buffer[80];

    time_t rawtime = [self timeIntervalSince1970] - [[NSTimeZone localTimeZone] secondsFromGMT];
    timeinfo = localtime(&rawtime);

    strftime(buffer, 80, "%Y-%m-%dT%H:%M:%S%z", timeinfo);

    return [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding];
}
Vincent NOCK
  • 205
  • 2
  • 6
  • Right now this is a Link-Only answer. This is problematic because links can die. Please copy (with attribution) the important details to your post. – JasonMArcher Nov 07 '14 at 18:33