iPhone: Convert date string to a relative time stamp
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];
}
Comments
-
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 over 14 yearsNote: 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 over 14 yearsIt can not handle internationalization. Wish Apple add relative time format to NSDateFormatter. Android has it.
-
Carl Coryell-Martin over 14 yearsto add internationalization, just wrap the strings with NSLocalizedString() and you're good to go for most cases.
-
Chris Ladd almost 13 yearsThis 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 almost 13 yearsOh, 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 over 12 yearsOld post, but I just used this. You are my hero!
-
Nick Forge about 12 years@andreb
NSCalendarDate
is being deprecated, butNSDateComponents
is certainly not. The right way to get these components from anNSDate
is to useNSCalendar
andNSDateComponents
, as shown in this answer.NSDateFormatter
s should be used to convert to/from string representations, not to get individual date components. -
benjammin almost 12 yearsFor 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 almost 12 yearsTo 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 over 11 yearsI think it'll be cool if it can say today, for example, or this month.
-
RyanG almost 11 yearsExactly what I was looking for.
-
kevinl over 10 yearsthanks 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 over 10 yearsI 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 over 9 yearsI think this method works quite well. Only thing, the conversion to time, shouldn't that be part of the function as well?
-
dimpiax over 9 yearsyou talk about getTimeData? I encapsulated it from class scope, and gave it lifetime in functions just, because in other cases it's useless.
-
Antoine over 9 yearsNo, I mean the calculation in your 'Usage' explanation. Shouldn't this part
NSDate(timeIntervalSince1970: timestamp).timeIntervalSinceNow * -1
be added to the mainfunctionrelativeTimeInString
as you will now always have to do that calculation before. -
dimpiax over 9 yearslike in independent case, you right that need to encapsulate this transformation. changed
-
tmr about 9 yearsi 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 about 9 yearsThanks @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 almost 9 yearsIt's worth mentioning that, as of iOS 8,
NSDateComponentsFormatter
will do this kind of conversion for you.