iPhone: Convert date string to a relative time stamp

43,457

Solution 1

-(NSString *)dateDiff:(NSString *)origDate {
    NSDateFormatter *df = [[NSDateFormatter alloc] init];
    [df setFormatterBehavior:NSDateFormatterBehavior10_4];
    [df setDateFormat:@"EEE, dd MMM yy HH:mm:ss VVVV"];
    NSDate *convertedDate = [df dateFromString:origDate];
    [df release];
    NSDate *todayDate = [NSDate date];
    double ti = [convertedDate timeIntervalSinceDate:todayDate];
    ti = ti * -1;
    if(ti < 1) {
        return @"never";
    } else  if (ti < 60) {
        return @"less than a minute ago";
    } else if (ti < 3600) {
        int diff = round(ti / 60);
        return [NSString stringWithFormat:@"%d minutes ago", diff];
    } else if (ti < 86400) {
        int diff = round(ti / 60 / 60);
        return[NSString stringWithFormat:@"%d hours ago", diff];
    } else if (ti < 2629743) {
        int diff = round(ti / 60 / 60 / 24);
        return[NSString stringWithFormat:@"%d days ago", diff];
    } else {
        return @"never";
    }   
}

Solution 2

Here are methods from Cocoa to help you to get relevant info (not sure if they are all available in coca-touch).

    NSDate * today = [NSDate date];
    NSLog(@"today: %@", today);

    NSString * str = @"Thu, 21 May 09 19:10:09 -0700";
    NSDate * past = [NSDate dateWithNaturalLanguageString:str
                            locale:[[NSUserDefaults 
                            standardUserDefaults] dictionaryRepresentation]];

    NSLog(@"str: %@", str);
    NSLog(@"past: %@", past);

    NSCalendar *gregorian = [[NSCalendar alloc]
                             initWithCalendarIdentifier:NSGregorianCalendar];
    unsigned int unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | 
                             NSDayCalendarUnit | 
                             NSHourCalendarUnit | NSMinuteCalendarUnit | 
                             NSSecondCalendarUnit;
    NSDateComponents *components = [gregorian components:unitFlags
                                                fromDate:past
                                                  toDate:today
                                                 options:0];

    NSLog(@"months: %d", [components month]);
    NSLog(@"days: %d", [components day]);
    NSLog(@"hours: %d", [components hour]);
    NSLog(@"seconds: %d", [components second]);

The NSDateComponents object seems to hold the difference in relevant units (as specified). If you specify all units you can then use this method:

void dump(NSDateComponents * t)
{
    if ([t year]) NSLog(@"%d years ago", [t year]);
    else if ([t month]) NSLog(@"%d months ago", [t month]);
    else if ([t day]) NSLog(@"%d days ago", [t day]);
    else if ([t minute]) NSLog(@"%d minutes ago", [t minute]);
    else if ([t second]) NSLog(@"%d seconds ago", [t second]);
}

If you want to calculate yourself you can have a look at:

NSDate timeIntervalSinceDate

And then use seconds in the algorithm.

Disclaimer: If this interface is getting deprecated (I haven't checked), Apple's preferred way of doing this via NSDateFormatters, as suggested in comments below, looks pretty neat as well - I'll keep my answer for historical reasons, it may still be useful for some to look at the logic used.

Solution 3

I can't edit yet, but I took Gilean's code and made a couple of tweaks and made it a category of NSDateFormatter.

It accepts a format string so it will work w/ arbitrary strings and I added if clauses to have singular events be grammatically correct.

Cheers,

Carl C-M

@interface NSDateFormatter (Extras)
+ (NSString *)dateDifferenceStringFromString:(NSString *)dateString
                                  withFormat:(NSString *)dateFormat;

@end

@implementation NSDateFormatter (Extras)

+ (NSString *)dateDifferenceStringFromString:(NSString *)dateString
                                  withFormat:(NSString *)dateFormat
{
  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
  [dateFormatter setDateFormat:dateFormat];
  NSDate *date = [dateFormatter dateFromString:dateString];
  [dateFormatter release];
  NSDate *now = [NSDate date];
  double time = [date timeIntervalSinceDate:now];
  time *= -1;
  if(time < 1) {
    return dateString;
  } else if (time < 60) {
    return @"less than a minute ago";
  } else if (time < 3600) {
    int diff = round(time / 60);
    if (diff == 1) 
      return [NSString stringWithFormat:@"1 minute ago", diff];
    return [NSString stringWithFormat:@"%d minutes ago", diff];
  } else if (time < 86400) {
    int diff = round(time / 60 / 60);
    if (diff == 1)
      return [NSString stringWithFormat:@"1 hour ago", diff];
    return [NSString stringWithFormat:@"%d hours ago", diff];
  } else if (time < 604800) {
    int diff = round(time / 60 / 60 / 24);
    if (diff == 1) 
      return [NSString stringWithFormat:@"yesterday", diff];
    if (diff == 7) 
      return [NSString stringWithFormat:@"last week", diff];
    return[NSString stringWithFormat:@"%d days ago", diff];
  } else {
    int diff = round(time / 60 / 60 / 24 / 7);
    if (diff == 1)
      return [NSString stringWithFormat:@"last week", diff];
    return [NSString stringWithFormat:@"%d weeks ago", diff];
  }   
}

@end

Solution 4

In the interest of completeness, based on a @Gilean's answer, here's the complete code for a simple category on NSDate that mimics rails' nifty date helpers. For a refresher on categories, these are instance methods that you would call on NSDate objects. So, if I have an NSDate that represents yesterday, [myDate distanceOfTimeInWordsToNow] => "1 day".

Hope it's useful!

@interface NSDate (NSDate_Relativity)

-(NSString *)distanceOfTimeInWordsSinceDate:(NSDate *)aDate;
-(NSString *)distanceOfTimeInWordsToNow;

@end



@implementation NSDate (NSDate_Relativity)


-(NSString *)distanceOfTimeInWordsToNow {
    return [self distanceOfTimeInWordsSinceDate:[NSDate date]];

}

-(NSString *)distanceOfTimeInWordsSinceDate:(NSDate *)aDate {
    double interval = [self timeIntervalSinceDate:aDate];

    NSString *timeUnit;
    int timeValue;

    if (interval < 0) {
        interval = interval * -1;        
    }

    if (interval< 60) {
        return @"seconds";

    } else if (interval< 3600) { // minutes

        timeValue = round(interval / 60);

        if (timeValue == 1) {
            timeUnit = @"minute";

        } else {
            timeUnit = @"minutes";

        }


    } else if (interval< 86400) {
        timeValue = round(interval / 60 / 60);

        if (timeValue == 1) {
            timeUnit = @"hour";

        } else {
            timeUnit = @"hours";
        }


    } else if (interval< 2629743) {
        int days = round(interval / 60 / 60 / 24);

        if (days < 7) {

            timeValue = days;

            if (timeValue == 1) {
                timeUnit = @"day";
            } else {
                timeUnit = @"days";
            }

        } else if (days < 30) {
            int weeks = days / 7;

            timeValue = weeks;

            if (timeValue == 1) {
                timeUnit = @"week";
            } else {
                timeUnit = @"weeks";
            }


        } else if (days < 365) {

            int months = days / 30;
            timeValue = months;

            if (timeValue == 1) {
                timeUnit = @"month";
            } else {
                timeUnit = @"months";
            }

        } else if (days < 30000) { // this is roughly 82 years. After that, we'll say 'forever'
            int years = days / 365;
            timeValue = years;

            if (timeValue == 1) {
                timeUnit = @"year";
            } else {
                timeUnit = @"years";
            }

        } else {
            return @"forever ago";
        }
    }

    return [NSString stringWithFormat:@"%d %@", timeValue, timeUnit];

}

@end

Solution 5

There are already a lot of answers that come to the same solution but it can't hurt to have choices. Here's what I came up with.

- (NSString *)stringForTimeIntervalSinceCreated:(NSDate *)dateTime
{
    NSDictionary *timeScale = @{@"second":@1,
                                @"minute":@60,
                                @"hour":@3600,
                                @"day":@86400,
                                @"week":@605800,
                                @"month":@2629743,
                                @"year":@31556926};
    NSString *scale;
    int timeAgo = 0-(int)[dateTime timeIntervalSinceNow];
    if (timeAgo < 60) {
        scale = @"second";
    } else if (timeAgo < 3600) {
        scale = @"minute";
    } else if (timeAgo < 86400) {
        scale = @"hour";
    } else if (timeAgo < 605800) {
        scale = @"day";
    } else if (timeAgo < 2629743) {
        scale = @"week";
    } else if (timeAgo < 31556926) {
        scale = @"month";
    } else {
        scale = @"year";
    }

    timeAgo = timeAgo/[[timeScale objectForKey:scale] integerValue];
    NSString *s = @"";
    if (timeAgo > 1) {
        s = @"s";
    } 
    return [NSString stringWithFormat:@"%d %@%@ ago", timeAgo, scale, s];
}
Share:
43,457
Madhusuthanan Seetharam
Author by

Madhusuthanan Seetharam

Web developer gilean at gmail

Updated on April 08, 2020

Comments

  • Madhusuthanan Seetharam
    Madhusuthanan Seetharam about 4 years

    I've got a timestamp as a string like:

    Thu, 21 May 09 19:10:09 -0700

    and I'd like to convert it to a relative time stamp like '20 minutes ago' or '3 days ago'.

    What's the best way to do this using Objective-C for the iPhone?

  • andreb
    andreb over 14 years
    Note: the old NSCalendarDate/NSDateComponents way is being deprecated on Mac OS X. Apple seems to recommend using NSDateFormatters exclusively now. They make it quite easy to juggle with any components. Also see Gilean's answer.
  • an0
    an0 over 14 years
    It can not handle internationalization. Wish Apple add relative time format to NSDateFormatter. Android has it.
  • Carl Coryell-Martin
    Carl Coryell-Martin over 14 years
    to add internationalization, just wrap the strings with NSLocalizedString() and you're good to go for most cases.
  • Chris Ladd
    Chris Ladd almost 13 years
    This is a handy method, FYI, to wrap into a category on NSDate. I have one called NSDate+Relativity. The methods are -(NSString *)distanceOfTimeInWordsSinceDate:(NSDate *)aDate, which does all the work, and the convenience method -(NSString *)distanceOfTimeInWordsToNow, which calls the former, with [NSDate date]
  • Chris Ladd
    Chris Ladd almost 13 years
    Oh, also: I left off 'ago' in this implementation to make it more useful. So you could call this and say @"it's been %@ since your last confession.", [confessionDate distanceOfTimeInWordsToNow]
  • iosfreak
    iosfreak over 12 years
    Old post, but I just used this. You are my hero!
  • Nick Forge
    Nick Forge about 12 years
    @andreb NSCalendarDate is being deprecated, but NSDateComponents is certainly not. The right way to get these components from an NSDate is to use NSCalendar and NSDateComponents, as shown in this answer. NSDateFormatters should be used to convert to/from string representations, not to get individual date components.
  • benjammin
    benjammin almost 12 years
    For the grammatically paranoid, the return statements can be augmented like so: return (diff == 1) ? [NSString stringWithFormat:@"%d minute ago", diff] : [NSString stringWithFormat:@"%d minutes ago", diff];
  • ozba
    ozba almost 12 years
    To add months and years add this code: else if (ti < 31536000) { int diff = round(ti / 60 / 60 / 24 / 30); return[NSString stringWithFormat:@"%d months ago", diff]; } else { int diff = round(ti / 60 / 60 / 24 / 365); return[NSString stringWithFormat:@"%d years ago", diff];
  • Anonymous White
    Anonymous White over 11 years
    I think it'll be cool if it can say today, for example, or this month.
  • RyanG
    RyanG almost 11 years
    Exactly what I was looking for.
  • kevinl
    kevinl over 10 years
    thanks for the method. I've used it so that it'll say the actual date once the %d days ago exceeds a specific number.
  • Admin
    Admin over 10 years
    I think it's bad for readability when you use hardcoded values in your conditions. You could just place a couple of #defines at the top like: #define MINUTE 60 #define HOUR 60 * 60 #define DAY 60 * 60 * 24 #define MONTH 60 * 60 * 24 * 30 & then check against these.
  • Antoine
    Antoine over 9 years
    I think this method works quite well. Only thing, the conversion to time, shouldn't that be part of the function as well?
  • dimpiax
    dimpiax over 9 years
    you talk about getTimeData? I encapsulated it from class scope, and gave it lifetime in functions just, because in other cases it's useless.
  • Antoine
    Antoine over 9 years
    No, I mean the calculation in your 'Usage' explanation. Shouldn't this part NSDate(timeIntervalSince1970: timestamp).timeIntervalSinceNow * -1 be added to the mainfunction relativeTimeInString as you will now always have to do that calculation before.
  • dimpiax
    dimpiax over 9 years
    like in independent case, you right that need to encapsulate this transformation. changed
  • tmr
    tmr about 9 years
    i do not know the orginal source of code in this answer, but the code worked well and the instructions on the github page were incredibly clear. thank you NV.
  • N V
    N V about 9 years
    Thanks @tmr! Really appreciate the comment that the instructions were clear - I try to write stuff so that its step by step and 100% clear and easy to understand, thanks again! :) @Hot Licks - I have no idea what you're talking about. I wrote 100% of the code. It was based off of how Facebook Mobile rendered their dates.
  • Srivathsalachary Vangeepuram
    Srivathsalachary Vangeepuram almost 9 years
    It's worth mentioning that, as of iOS 8, NSDateComponentsFormatter will do this kind of conversion for you.