NSDateFormatter & Http Header

Tue, 18. Aug 2009

Categories: en development Tags: Asctime Cocoa Date HTTP Header If-Modified-Since iPhone Last-Modified NSDate NSDateFormatter NSHTTPURLResponse Objective C Request Header Response Header RFC1123 RFC850 SenTestingKit

A.S. Update 2, License: as I’ve been asked about which license this code is under: I put this into Public Domain. No warranty whatsoever. Still I’d be happy about attribution but don’t require such.

Ever tried to get e.g. the Last-Modified HTTP response header field into a NSDate object? That’s no real fun, because

  1. this standard formatting isn’t digested by default,
  2. the required format string doesn’t quite work as documented.

Let’s concentrate on ‘Full Date’ according to RFC 1123:

NSString *src = @"Fri, 14 Aug 2009 14:45:31 GMT";

The NSDateFormatter switched to Unicode Format strings with OSX 10.4 (see the reference), so I use

NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
df.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";

Caution: zzz as timezone format should give the same results, but doesn’t. It says ...GMT+00:00.

As the textual parts must be english (see RFC 1123 for all the wording), I add

df.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease];

to be independent of the user’s current locale.

That’s it!

Update:

If [NSDateFormatter dateFromString:...] and [NSDateFormatter stringFromDate:...] are threadsafe, a category-implementation could look like:

#import 

/** Category on NSDate to support rfc1123 formatted date strings.
 http://blog.mro.name/2009/08/nsdateformatter-http-header/ and
 http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
 */
@interface NSDate (NSDateRFC1123)

/**
 Convert a RFC1123 'Full-Date' string
 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1)
 into NSDate.
 */
+(NSDate*)dateFromRFC1123:(NSString*)value_;

/**
 Convert NSDate into a RFC1123 'Full-Date' string
 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1).
 */
-(NSString*)rfc1123String;

@end

@implementation NSDate (NSDateRFC1123)

+(NSDate*)dateFromRFC1123:(NSString*)value_
{
    if(value_ == nil)
        return nil;
    static NSDateFormatter *rfc1123 = nil;
    if(rfc1123 == nil)
    {
        rfc1123 = [[NSDateFormatter alloc] init];
        rfc1123.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease];
        rfc1123.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
        rfc1123.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss z";
    }
    NSDate *ret = [rfc1123 dateFromString:value_];
    if(ret != nil)
        return ret;

    static NSDateFormatter *rfc850 = nil;
    if(rfc850 == nil)
    {
        rfc850 = [[NSDateFormatter alloc] init];
        rfc850.locale = rfc1123.locale;
        rfc850.timeZone = rfc1123.timeZone;
        rfc850.dateFormat = @"EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z";
    }
    ret = [rfc850 dateFromString:value_];
    if(ret != nil)
        return ret;

    static NSDateFormatter *asctime = nil;
    if(asctime == nil)
    {
        asctime = [[NSDateFormatter alloc] init];
        asctime.locale = rfc1123.locale;
        asctime.timeZone = rfc1123.timeZone;
        asctime.dateFormat = @"EEE MMM d HH':'mm':'ss yyyy";
    }
    return [asctime dateFromString:value_];
}

-(NSString*)rfc1123String
{
    static NSDateFormatter *df = nil;
    if(df == nil)
    {
        df = [[NSDateFormatter alloc] init];
        df.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease];
        df.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
        df.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
    }
    return [df stringFromDate:self];
}

@end

//////////////////////////////////////////////////////////
// A testcase to try things out.
//////////////////////////////////////////////////////////

#import 

/**
 http://blog.mro.name/2009/08/nsdateformatter-http-header/ and
 http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
 */
@interface NSDateFormatterTC : SenTestCase
{

}

@end

@implementation NSDateFormatterTC

-(void)testAsctime
{
    STAssertEqualObjects(@"Sun, 06 Nov 1994 08:49:37 GMT",
        [[NSDate dateFromRFC1123:@"Sun Nov  6 08:49:37 1994"] rfc1123String], @"fail");
    STAssertEqualObjects(@"Wed, 16 Nov 1994 08:49:37 GMT",
        [[NSDate dateFromRFC1123:@"Wed Nov 16 08:49:37 1994"] rfc1123String], @"fail");
}

-(void)testRFC850
{
    STAssertEqualObjects(@"Sun, 06 Nov 1994 08:49:37 GMT",
        [[NSDate dateFromRFC1123:@"Sunday, 06-Nov-94 08:49:37 GMT"] rfc1123String], @"fail");
}

-(void)testRFC1123
{
    NSString *s = @"Fri, 14 Aug 2009 14:45:31 GMT";
    STAssertEqualObjects(s, [[NSDate dateFromRFC1123:s] rfc1123String], @"fail");

    s = @"Sun, 06 Nov 1994 08:49:37 GMT";
    STAssertEqualObjects(s, [[NSDate dateFromRFC1123:s] rfc1123String], @"fail");

    // a strange asymmetry:
    s = @"Sat, 01 Jan 0001 00:00:00 GMT";
    STAssertEqualObjects(@"Sat, 01 Jan 0001 01:27:48 GMT",
        [[NSDate dateFromRFC1123:s] rfc1123String], @"fail");
}

-(void)testRawRFC1123
{
    NSDateFormatter *rfc1123 = [[NSDateFormatter alloc] init];
    rfc1123.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"] autorelease];
    rfc1123.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
    rfc1123.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";

    NSString *s = @"Fri, 14 Aug 2009 14:45:31 GMT";
    STAssertEqualObjects(s, [rfc1123 stringFromDate:[rfc1123 dateFromString:s]], @"fail");

    s = @"Sun, 06 Nov 1994 08:49:37 GMT";
    STAssertEqualObjects(s, [rfc1123 stringFromDate:[rfc1123 dateFromString:s]], @"fail");

    s = @"Sat, 01 Jan 0001 08:00:00 GMT";
    STAssertEqualObjects(s, [rfc1123 stringFromDate:[rfc1123 dateFromString:s]], @"fail");

    [rfc1123 release];
}
@end