39

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?

stefanB
  • 77,323
  • 27
  • 116
  • 141
Gilean
  • 14,708
  • 10
  • 45
  • 52
  • 1
    Duplicate of this question. http://stackoverflow.com/questions/822124/fuzzy-date-algorithm – Marc W May 24 '09 at 03:15
  • ... which in turn links to this question: http://stackoverflow.com/questions/11 (yes, question #11) – Dave DeLong Nov 17 '10 at 21:24
  • I found this post useful: [converting nstimeinterval into minutes seconds etc][1] [1]: http://stackoverflow.com/questions/1189252/how-to-convert-an-nstimeinterval-seconds-into-minutes –  Jul 25 '11 at 18:03
  • check [this answer](http://stackoverflow.com/a/34359788/1106035) which has sample link. – Paresh Navadiya Dec 18 '15 at 16:26
  • If you want the time ago date format to be exactly like Facebook Mobile, here's a link to a library I wrote a while ago to do it: https://github.com/nikilster/NSDate-Time-Ago – N V Dec 25 '15 at 02:18

11 Answers11

72
-(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";
    }   
}
Gilean
  • 14,708
  • 10
  • 45
  • 52
  • It can not handle internationalization. Wish Apple add relative time format to NSDateFormatter. Android has it. – an0 Nov 09 '09 at 07:00
  • 2
    to add internationalization, just wrap the strings with NSLocalizedString() and you're good to go for most cases. – Carl Coryell-Martin Nov 12 '09 at 10:00
  • 2
    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 Jun 28 '11 at 15:01
  • Old post, but I just used this. You are my hero! – iosfreak Dec 27 '11 at 15:45
  • 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];` – benjammin Jul 24 '12 at 05:50
  • 2
    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]; – ozba Aug 31 '12 at 10:33
  • I think it'll be cool if it can say today, for example, or this month. – Anonymous White Oct 26 '12 at 04:41
  • 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. – kevinl Dec 13 '13 at 17:00
  • 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. –  Dec 25 '13 at 09:17
  • It's worth mentioning that, as of iOS 8, `NSDateComponentsFormatter` will do this kind of conversion for you. – Tom Harrington Jul 29 '15 at 02:29
22

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.

stefanB
  • 77,323
  • 27
  • 116
  • 141
  • 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. – andreb Oct 26 '09 at 15:02
  • 1
    @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. `NSDateFormatter`s should be used to convert to/from string representations, not to get individual date components. – Nick Forge Apr 04 '12 at 07:49
14

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
Carl Coryell-Martin
  • 3,410
  • 3
  • 26
  • 23
8

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
Chris Ladd
  • 2,795
  • 1
  • 29
  • 22
  • 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] – Chris Ladd Jun 28 '11 at 15:37
6

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];
}
Dean Kelly
  • 598
  • 7
  • 13
4

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
malhal
  • 26,330
  • 7
  • 115
  • 133
3

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"
        }
    }
}
dimpiax
  • 12,093
  • 5
  • 62
  • 45
  • I think this method works quite well. Only thing, the conversion to time, shouldn't that be part of the function as well? – Antoine Feb 20 '15 at 09:27
  • you talk about getTimeData? I encapsulated it from class scope, and gave it lifetime in functions just, because in other cases it's useless. – dimpiax Feb 20 '15 at 23:39
  • 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. – Antoine Feb 23 '15 at 16:23
  • like in independent case, you right that need to encapsulate this transformation. *changed* – dimpiax Feb 23 '15 at 23:38
1

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;
}
ppaulojr
  • 3,579
  • 4
  • 29
  • 56
1

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...

Nael El Shawwa
  • 348
  • 1
  • 10
0

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

N V
  • 742
  • 1
  • 6
  • 21
  • 1
    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. – tmr Jun 24 '15 at 01:40
  • 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. – N V Jun 24 '15 at 01:51
0

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.

Bluephlame
  • 3,901
  • 4
  • 35
  • 51