Relative string from NSDate

Date math sucks. Check out Dave Delong's Chronology library (https://github.com/davedelong/Chronology), or, for iOS 13+, (NS)RelativeDateTimeFormatter

I'm leaving the below for posterity, but it is decidedly the Wrong Thing To Do. My updated answer is above.

Here's an NSDate category method I threw together that uses the Stack Overflow relativity calculation linked by David as well as other tips cobbled from other similar SO questions:

- (NSString *)relativeDateString
{
    const int SECOND = 1;
    const int MINUTE = 60 * SECOND;
    const int HOUR = 60 * MINUTE;
    const int DAY = 24 * HOUR;
    const int MONTH = 30 * DAY;

    NSDate *now = [NSDate date];
    NSTimeInterval delta = [self timeIntervalSinceDate:now] * -1.0;

    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSUInteger units = (NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit);
    NSDateComponents *components = [calendar components:units fromDate:self toDate:now options:0];

    NSString *relativeString;

    if (delta < 0) {
        relativeString = @"!n the future!";

    } else if (delta < 1 * MINUTE) {
        relativeString = (components.second == 1) ? @"One second ago" : [NSString stringWithFormat:@"%d seconds ago",components.second];

    } else if (delta < 2 * MINUTE) {
        relativeString =  @"a minute ago";

    } else if (delta < 45 * MINUTE) {
        relativeString = [NSString stringWithFormat:@"%d minutes ago",components.minute];

    } else if (delta < 90 * MINUTE) {
        relativeString = @"an hour ago";

    } else if (delta < 24 * HOUR) {
        relativeString = [NSString stringWithFormat:@"%d hours ago",components.hour];

    } else if (delta < 48 * HOUR) {
        relativeString = @"yesterday";

    } else if (delta < 30 * DAY) {
        relativeString = [NSString stringWithFormat:@"%d days ago",components.day];

    } else if (delta < 12 * MONTH) {
        relativeString = (components.month <= 1) ? @"one month ago" : [NSString stringWithFormat:@"%d months ago",components.month];

    } else {
        relativeString = (components.year <= 1) ? @"one year ago" : [NSString stringWithFormat:@"%d years ago",components.year];

    }

    return relativeString;
}

DO NOT use your own custom string translation. Writing your own translation that does something like "3 seconds ago" not only has the problem of needing to be updated constantly, but it also does not localize into other languages.

Instead, use Apple's built in NSDateFormatter:

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.doesRelativeDateFormatting = YES;
formatter.locale = [NSLocale currentLocale];
formatter.dateStyle = NSDateFormatterShortStyle;
formatter.timeStyle = NSDateFormatterShortStyle;
NSString *timeString = [formatter stringFromDate:self.date];

This will output something like "Today, 11:10 PM", or "7/17/14, 5:44 AM".


It appears that TTTTimeIntervalFormatter in FormatterKit can also help with generating relative time interval strings.


NSDateFormatter will do a great deal of what is mentioned above by using setDoesRelativeDateFormatting: on an instance. From there, you can control the formatting using the date style and the time style to fine tune it to your needs.

If that doesn't get you what you need then check out SORelativeDateTransformer, it is an NSValueTransformer subclass that attempts to provide the same behavior you see with relative dates on SO.

Finally, if you want to roll your own or do more research here is the (likely) original question on the topic... yes it is question 11.