Tutorial :iPhone: Convert date string to a relative time stamp



Question:

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?


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];  }  


Solution:6

I took Carl Coryell-Martin's code and made a simpler NSDate category that doesn't have warnings about the string formatting of the singulars, and also tidys up the week ago singular:

@interface NSDate (Extras)  - (NSString *)differenceString;  @end    @implementation NSDate (Extras)    - (NSString *)differenceString{      NSDate* date = self;      NSDate *now = [NSDate date];      double time = [date timeIntervalSinceDate:now];      time *= -1;      if (time < 60) {          int diff = round(time);          if (diff == 1)              return @"1 second ago";          return [NSString stringWithFormat:@"%d seconds ago", diff];      } else if (time < 3600) {          int diff = round(time / 60);          if (diff == 1)              return @"1 minute ago";          return [NSString stringWithFormat:@"%d minutes ago", diff];      } else if (time < 86400) {          int diff = round(time / 60 / 60);          if (diff == 1)              return @"1 hour ago";          return [NSString stringWithFormat:@"%d hours ago", diff];      } else if (time < 604800) {          int diff = round(time / 60 / 60 / 24);          if (diff == 1)              return @"yesterday";          if (diff == 7)              return @"a week ago";          return[NSString stringWithFormat:@"%d days ago", diff];      } else {          int diff = round(time / 60 / 60 / 24 / 7);          if (diff == 1)              return @"a week ago";          return [NSString stringWithFormat:@"%d weeks ago", diff];      }     }    @end  


Solution:7

In Swift

Usage:

let time = NSDate(timeIntervalSince1970: timestamp).timeIntervalSinceNow  let relativeTimeString = NSDate.relativeTimeInString(time)  println(relativeTimeString)  

Extension:

extension NSDate {      class func relativeTimeInString(value: NSTimeInterval) -> String {          func getTimeData(value: NSTimeInterval) -> (count: Int, suffix: String) {              let count = Int(floor(value))              let suffix = count != 1 ? "s" : ""              return (count: count, suffix: suffix)          }            let value = -value          switch value {              case 0...15: return "just now"                case 0..<60:                  let timeData = getTimeData(value)                  return "\(timeData.count) second\(timeData.suffix) ago"                case 0..<3600:                  let timeData = getTimeData(value/60)                  return "\(timeData.count) minute\(timeData.suffix) ago"                case 0..<86400:                  let timeData = getTimeData(value/3600)                  return "\(timeData.count) hour\(timeData.suffix) ago"                case 0..<604800:                  let timeData = getTimeData(value/86400)                  return "\(timeData.count) day\(timeData.suffix) ago"                default:                  let timeData = getTimeData(value/604800)                  return "\(timeData.count) week\(timeData.suffix) ago"          }      }  }  


Solution:8

Use the NSDate class:

timeIntervalSinceDate  

returns the interval in seconds.

Quick exercise to implement this in objective-c:

  1. Get time "now" NSDate
  2. Get the NSDate you wish to compare with
  3. Get the interval in seconds using timeIntervalSinceDate

Then implement this pseudo code:

if (x < 60) // x seconds ago    else if( x/60 < 60) // floor(x/60) minutes ago    else if (x/(60*60) < 24) // floor(x/(60*60) hours ago    else if (x/(24*60*60) < 7) // floor(x(24*60*60) days ago  

and so on...

then you need to decide whether a month is 30,31 or 28 days. Keep it simple - pick 30.

There might be a better way, but its 2am and this is the first thing that came to mind...


Solution:9

My solution:

- (NSString *) dateToName:(NSDate*)dt withSec:(BOOL)sec {        NSLocale *locale = [NSLocale currentLocale];      NSTimeInterval tI = [[NSDate date] timeIntervalSinceDate:dt];      if (tI < 60) {        if (sec == NO) {             return NSLocalizedString(@"Just Now", @"");         }         return [NSString stringWithFormat:                   NSLocalizedString(@"%d seconds ago", @""),(int)tI];       }       if (tI < 3600) {         return [NSString stringWithFormat:                   NSLocalizedString(@"%d minutes ago", @""),(int)(tI/60)];       }       if (tI < 86400) {        return [NSString stringWithFormat:                   NSLocalizedString(@"%d hours ago", @""),(int)tI/3600];       }         NSDateFormatter *relativeDateFormatter = [[NSDateFormatter alloc] init];       [relativeDateFormatter setTimeStyle:NSDateFormatterNoStyle];       [relativeDateFormatter setDateStyle:NSDateFormatterMediumStyle];       [relativeDateFormatter setDoesRelativeDateFormatting:YES];       [relativeDateFormatter setLocale:locale];         NSString * relativeFormattedString =               [relativeDateFormatter stringForObjectValue:dt];       return relativeFormattedString;  }  


Solution:10

Not sure why this isnt in cocoa-touch, i nice standard way of doing this would be great.

Set up some types to keep the data in, it will make it easier if you ever ned to localise it a bit more. (obviously expand if you need more time periods)

typedef struct DayHours {      int Days;      double Hours;  } DayHours;      + (DayHours) getHourBasedTimeInterval:(double) hourBased withHoursPerDay:(double) hpd  {      int NumberOfDays = (int)(fabs(hourBased) / hpd);      float hoursegment = fabs(hourBased) - (NumberOfDays * hpd);      DayHours dh;      dh.Days = NumberOfDays;      dh.Hours = hoursegment;      return dh;  }  

NOTE: I"m using an hour based calculation , as that is what my data is in. NSTimeInterval is second based. I also had to convert between the two.


Solution:11

I saw that there were several time ago functions in snippets of code on Stack Overflow and I wanted one that really gave the clearest sense of the time (since some action occurred). To me this means "time ago" style for short time intervals (5 min ago, 2 hours ago) and specific dates for longer time periods (April 15, 2011 instead of 2 years ago). Basically I thought Facebook did a really good job at this and I wanted to just go by their example (as I'm sure they out a lot of thought into this and it is very easy and clear to understand from the consumer perspective).

After a long time of googling I was pretty surprised to see that no one had implemented this as far as I could tell. Decided that I wanted it bad enough to spend the time writing and thought that I would share.

Hope you enjoy :)

Get the code here: https://github.com/nikilster/NSDate-Time-Ago


Note:If u also have question or solution just comment us below or mail us on toontricks1994@gmail.com
Previous
Next Post »