Date Formatters

There are two basic methods you use to create a string representation of a date and to parse a string to get a date object using a date formatter—dateFromString: and stringFromDate: respectively. You can also use getObjectValue:forString:range:error: if you need more control over the range of a string you want to parse.

There are many attributes you can get and set on a date formatter. When you present information to the user, you should typically simply use the NSDateFormatter style constants to specify pre-defined sets of attributes that determine how a formatted date is displayed. If you need to generate a representation of a date in a precise format, however, you should use a format string.

If you need to parse a date string, the approach you take again depends on what you want to accomplish. If you want to parse input from the user, you should typically use the style constants so as to match their expectations. If you want to parse dates you get from a database or a web service, for example, you should use a format string.

In all cases, you should consider that formatters default to using the user’s locale (currentLocale) superimposed with the user’s preference settings. If you want to use the user’s locale but without their individual settings, you can get the locale id from the current user locale (localeIdentifier) and make a new "standard” locale with that, then set the standard locale as the formatter’s locale.

Use Formatter Styles to Present Dates and Times With the User’s Preferences

NSDateFormatter makes it easy for you to format a date using the settings a user configured in the International preferences panel in System Preferences. The NSDateFormatter style constants—NSDateFormatterNoStyle, NSDateFormatterShortStyle, NSDateFormatterMediumStyle, NSDateFormatterLongStyle, and NSDateFormatterFullStyle—specify sets of attributes that determine how a date is displayed according to the user’s preferences.

You specify the style of the date and time components of a date formatter independently using setDateStyle: and setTimeStyle: respectively. Listing 1 illustrates how you can format a date using formatter styles. Notice the use of NSDateFormatterNoStyle to suppress the time component and yield a string containing just the date.

Listing 1  Formatting a date using formatter styles

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
 
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:162000];
 
NSString *formattedDateString = [dateFormatter stringFromDate:date];
NSLog(@"formattedDateString: %@", formattedDateString);
// Output for locale en_US: "formattedDateString: Jan 2, 2001".

Use Format Strings to Specify Custom Formats

There are broadly speaking two situations in which you need to use custom formats:

  1. For fixed format strings, like Internet dates.

  2. For user-visible elements that don’t match any of the existing styles

Fixed Formats

To specify a custom fixed format for a date formatter, you use setDateFormat:. The format string uses the format patterns from the Unicode Technical Standard #35. The version of the standard varies with release of the operating system:

Although in principle a format string specifies a fixed format, by default NSDateFormatter still takes the user’s preferences (including the locale setting) into account. You must consider the following points when using format strings:

  • NSDateFormatter treats the numbers in a string you parse as if they were in the user’s chosen calendar. For example, if the user selects the Buddhist calendar, parsing the year 2010 yields an NSDate object in 1467 in the Gregorian calendar. (For more about different calendrical systems and how to use them, see Date and Time Programming Guide.)

  • In iOS, the user can override the default AM/PM versus 24-hour time setting. This may cause NSDateFormatter to rewrite the format string you set.

Note with the Unicode format string format, you should enclose literal text in the format string between apostrophes ('').

The following example illustrates using a format string to generate a string:

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd 'at' HH:mm"];
 
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:162000];
 
NSString *formattedDateString = [dateFormatter stringFromDate:date];
NSLog(@"formattedDateString: %@", formattedDateString);
// For US English, the output may be:
// formattedDateString: 2001-01-02 at 13:00

There are two things to note about this example:

  1. It uses yyyy to specify the year component. A common mistake is to use YYYY. yyyy specifies the calendar year whereas YYYY specifies the year (of “Week of Year”), used in the ISO year-week calendar. In most cases, yyyy and YYYY yield the same number, however they may be different. Typically you should use the calendar year.

  2. The representation of the time may be 13:00. In iOS, however, if the user has switched 24-Hour Time to Off, the time may be 1:00 pm.

Custom Formats for User-Visible Dates

To display a date that contains a specific set of elements, you use dateFormatFromTemplate:options:locale:]. The method generates a format string with the date components you want to use, but with the correct punctuation and order appropriate for the user (that is, customized for the user’s locale and preferences). You then use the format string to create a formatter.

For example, to create a formatter to display today’s day name, day, and month using the current locale, you would write:

NSString *formatString = [NSDateFormatter dateFormatFromTemplate:@"EdMMM" options:0
                                          locale:[NSLocale currentLocale]];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:formatString];
 
NSString *todayString = [dateFormatter stringFromDate:[NSDate date]];
NSLog(@"todayString: %@", todayString);

To understand the need for this, consider a situation where you want to display the day name, day, and month. You cannot create this representation of a date using formatter styles (there is no style that omits the year). Neither, though, can you easily and consistently create the representation correctly using format strings. Although at first glance it may seem straightforward, there’s a complication: a user from the United States would typically expect dates in the form, “Mon, Jan 3”, whereas a user from Great Britain would typically expect dates in the form “Mon 31 Jan”.

The following example illustrates the point:

NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
NSString *usFormatString = [NSDateFormatter dateFormatFromTemplate:@"EdMMM" options:0 locale:usLocale];
NSLog(@"usFormatterString: %@", usFormatString);
// Output: usFormatterString: EEE, MMM d.
 
NSLocale *gbLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
NSString *gbFormatString = [NSDateFormatter dateFormatFromTemplate:@"EdMMM" options:0 locale:gbLocale];
NSLog(@"gbFormatterString: %@", gbFormatString);
// Output: gbFormatterString: EEE d MMM.

Parsing Date Strings

In addition to the methods inherited from NSFormatter (such as getObjectValue:forString:errorDescription:), NSDateFormatter adds dateFromString: and getObjectValue:forString:range:error:. These methods make it easier for you to use an NSDateFormatter object directly in code, and make it easier to format dates into strings more complex and more convenient ways than NSString formatting allows.

The getObjectValue:forString:range:error: method allows you to specify a subrange of the string to be parsed, and it returns the range of the string that was actually parsed (in the case of failure, it indicates where the failure occurred). It also returns an NSError object that can contain richer information than the failure string returned by the getObjectValue:forString:errorDescription: method inherited from NSFormatter.

If you're working with fixed-format dates, you should first set the locale of the date formatter to something appropriate for your fixed format. In most cases the best locale to choose is en_US_POSIX, a locale that's specifically designed to yield US English results regardless of both user and system preferences. en_US_POSIX is also invariant in time (if the US, at some point in the future, changes the way it formats dates, en_US will change to reflect the new behavior, but en_US_POSIX will not), and between platforms (en_US_POSIX works the same on iPhone OS as it does on OS X, and as it does on other platforms).

Once you’ve set en_US_POSIX as the locale of the date formatter, you can then set the date format string and the date formatter will behave consistently for all users.

Listing 2 shows how to use NSDateFormatter for both of the roles described above. First it creates a en_US_POSIX date formatter to parse the incoming RFC 3339 date string, using a fixed date format string and UTC as the time zone. Next, it creates a standard date formatter to render the date as a string to display to the user.

Listing 2  Parsing an RFC 3339 date-time

- (NSString *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString {
    /*
      Returns a user-visible date time string that corresponds to the specified
      RFC 3339 date time string. Note that this does not handle all possible
      RFC 3339 date time strings, just one of the most common styles.
     */
 
    NSDateFormatter *rfc3339DateFormatter = [[NSDateFormatter alloc] init];
    NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
 
    [rfc3339DateFormatter setLocale:enUSPOSIXLocale];
    [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
    [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
 
    // Convert the RFC 3339 date time string to an NSDate.
    NSDate *date = [rfc3339DateFormatter dateFromString:rfc3339DateTimeString];
 
    NSString *userVisibleDateTimeString;
    if (date != nil) {
        // Convert the date object to a user-visible date string.
        NSDateFormatter *userVisibleDateFormatter = [[NSDateFormatter alloc] init];
        assert(userVisibleDateFormatter != nil);
 
        [userVisibleDateFormatter setDateStyle:NSDateFormatterShortStyle];
        [userVisibleDateFormatter setTimeStyle:NSDateFormatterShortStyle];
 
        userVisibleDateTimeString = [userVisibleDateFormatter stringFromDate:date];
    }
    return userVisibleDateTimeString;
}

Cache Formatters for Efficiency

Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable.

Listing 3 re-implements the method shown in Listing 2 to hold on to the date formatters for subsequent reuse.

Listing 3  Parsing an RFC 3339 date-time using a cached formatter

static NSDateFormatter *sUserVisibleDateFormatter = nil;
 
- (NSString *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString {
    /*
      Returns a user-visible date time string that corresponds to the specified
      RFC 3339 date time string. Note that this does not handle all possible
      RFC 3339 date time strings, just one of the most common styles.
     */
 
    // If the date formatters aren't already set up, create them and cache them for reuse.
    static NSDateFormatter *sRFC3339DateFormatter = nil;
    if (sRFC3339DateFormatter == nil) {
        sRFC3339DateFormatter = [[NSDateFormatter alloc] init];
        NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
 
        [sRFC3339DateFormatter setLocale:enUSPOSIXLocale];
        [sRFC3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
        [sRFC3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
    }
 
    // Convert the RFC 3339 date time string to an NSDate.
    NSDate *date = [rfc3339DateFormatter dateFromString:rfc3339DateTimeString];
 
    NSString *userVisibleDateTimeString;
    if (date != nil) {
        if (sUserVisibleDateFormatter == nil) {
            sUserVisibleDateFormatter = [[NSDateFormatter alloc] init];
            [sUserVisibleDateFormatter setDateStyle:NSDateFormatterShortStyle];
            [sUserVisibleDateFormatter setTimeStyle:NSDateFormatterShortStyle];
        }
        // Convert the date object to a user-visible date string.
        userVisibleDateTimeString = [sUserVisibleDateFormatter stringFromDate:date];
    }
    return userVisibleDateTimeString;
}

If you cache date formatters (or any other objects that depend on the user’s current locale), you should subscribe to the NSCurrentLocaleDidChangeNotification notification and update your cached objects when the current locale changes. The code in Listing 3 defines sUserVisibleDateFormatter outside of the method so that other code, not shown, can update it as necessary. In contrast, sRFC3339DateFormatter is defined inside the method because, by design, it is not dependent on the user’s locale settings.

Consider Unix Functions for Fixed-Format, Unlocalized Dates

For date and times in a fixed, unlocalized format, that are always guaranteed to use the same calendar, it may sometimes be easier and more efficient to use the standard C library functions strptime_l and strftime_l.

Be aware that the C library also has the idea of a current locale. To guarantee a fixed date format, you should pass NULL as the loc parameter of these routines. This causes them to use the POSIX locale (also known as the C locale), which is equivalent to Cocoa's en_US_POSIX locale, as illustrated in this example.

struct tm  sometime;
const char *formatString = "%Y-%m-%d %H:%M:%S %z";
(void) strptime_l("2005-07-01 12:00:00 -0700", formatString, &sometime, NULL);
NSLog(@"NSDate is %@", [NSDate dateWithTimeIntervalSince1970: mktime(&sometime)]);
// Output: NSDate is 2005-07-01 12:00:00 -0700