Relative date formatting like Mail.app

Mail.app on Mac OS has a cool way of showing you relative dates of messages. Messages received today show only the time (e.g., "12:55 PM"), whereas messages received prior to today show only the date (e.g., 3/13/14), and if the date was Yesterday, it shows that, rather than the date. I wanted this same functionality in one of my apps.

Googling around for a bit turned up this StackOverflow question, with which I was not happy. So I did it myself (and, of course, added my own answer to that question). Here's what I did.

NSDateFormatter (as of Mac OS 10.6 and iOS4) does relative date formatting (e.g., "Today", "Yesterday" for you, like so:

NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setTimeStyle:NSDateFormatterNoStyle];
[formatter setDateStyle:NSDateFormatterShortStyle];
[formatter setDoesRelativeDateFormatting:YES];

NSString* dateString = [formatter stringFromDate:[NSDate date]];    
NSLog( @"date = %@", dateString );

Which outputs:

2014-03-15 15:26:37.683 TestApp[1293:303] date = Today

But that still leaves the issue of instead using the time if the date is today. First, we need to be able to tell if a given date is today. For this I implemented a method isToday, as a category on NSDate. (Side note: I freaking love categories!)

@implementation NSDate (IsToday)

- (BOOL) isToday
{
    NSCalendar* calendar = [NSCalendar currentCalendar];

    // Components representing the day of our date.
    NSDateComponents* dateComp = [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit
                                             fromDate:self];
    NSDate* date = [calendar dateFromComponents:dateComp];

    // Components representing today.
    NSDateComponents* todayComp = [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit
                                              fromDate:[NSDate date]];
    NSDate* todayDate = [calendar dateFromComponents:todayComp];

    // If the dates are equal, then our date is today.
    return [date isEqualToDate:todayDate];
}

@end

Then, I made two date formatters, one for the day, and one for the time, and decided which to use based on whether the date is today or not. Like so:

- (NSString*) postDateToString:(NSDate*)aDate
{
    static NSDateFormatter* todayFormatter = nil;
    if( todayFormatter == nil ) {
        todayFormatter = [[NSDateFormatter alloc] init];
        [todayFormatter setTimeStyle:NSDateFormatterShortStyle];
        [todayFormatter setDateStyle:NSDateFormatterNoStyle];
    }

    static NSDateFormatter* notTodayFormatter = nil;
    if( notTodayFormatter == nil ) {
        notTodayFormatter = [[NSDateFormatter alloc] init];
        [notTodayFormatter setTimeStyle:NSDateFormatterNoStyle];
        [notTodayFormatter setDateStyle:NSDateFormatterShortStyle];
        [notTodayFormatter setDoesRelativeDateFormatting:YES];
    }

    NSDateFormatter* formatter = notTodayFormatter;

    if( [aDate isToday] ) {
        formatter = todayFormatter;
    }

    return [formatter stringFromDate:aDate];
}

And there you have it.

If you want to follow me, I'm @zpasternack on Twitter and on app.net.