iPhone:将日期字符串转换为相对时间戳

39

我有一个时间戳字符串,如下:

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

我想将它转换成类似于“20分钟前”或“3天前”的相对时间戳。

在iPhone上使用Objective-C,最好的方法是什么?


1
这个问题已经有人问过了。 https://dev59.com/s3RA5IYBdhLWcg3wzhNY - Marc W
...它又链接到这个问题:https://dev59.com/b3VD5IYBdhLWcg3wXamd(是的,第11个问题) - Dave DeLong
我发现这篇帖子很有用:将NSTimeInterval转换为分钟、秒等 - user862127
请查看此答案,其中包含示例链接:http://stackoverflow.com/a/34359788/1106035。 - Paresh Navadiya
如果您想让时间格式与Facebook Mobile完全相同,这里是我之前编写的一个库的链接:https://github.com/nikilster/NSDate-Time-Ago - N V
11个回答

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

2
要添加国际化,只需使用NSLocalizedString()包装字符串,对于大多数情况来说,这样就可以了。 - Carl Coryell-Martin
2
这是一个方便的方法,供您参考,用于将其封装到NSDate的类别中。我有一个叫做NSDate+Relativity的类别。其中的方法包括-(NSString *)distanceOfTimeInWordsSinceDate:(NSDate *)aDate,它完成了所有的工作,以及一个便利方法-(NSString *)distanceOfTimeInWordsToNow,它调用前者,并使用[NSDate date]作为参数。 - Chris Ladd
2
要添加月份和年份,请添加以下代码:else if (ti < 31536000) { int diff = round(ti / 60 / 60 / 24 / 30); return[NSString stringWithFormat:@"%d个月前", diff]; } else { int diff = round(ti / 60 / 60 / 24 / 365); return[NSString stringWithFormat:@"%d年前", diff]; - ozba
我认为在条件语句中使用硬编码的值会影响代码的可读性。你可以在代码开头定义一些宏,例如: #define MINUTE 60 #define HOUR 60 * 60 #define DAY 60 * 60 * 24 #define MONTH 60 * 60 * 24 * 30,然后在代码中使用这些宏进行比较。 - user376845
值得一提的是,从iOS 8开始,NSDateComponentsFormatter将为您执行此类转换。 - Tom Harrington
显示剩余5条评论

22

以下是Cocoa提供的方法,可以帮助您获取相关信息(不确定它们是否都适用于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]);

NSDateComponents对象似乎保存相应单位的差值(如所指定)。 如果您指定了所有的单位,那么可以使用这个方法:

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

如果您想自行计算,可以查看以下内容:

NSDate timeIntervalSinceDate

然后在算法中使用秒。

免责声明:如果此接口被弃用(我没有检查),苹果推荐的方法是通过NSDateFormatters,如下面的评论所建议的那样,看起来也很不错 - 我将保留我的答案,出于历史原因,对于某些人来说仍然有用。


注意:在Mac OS X上,旧的NSCalendarDate/NSDateComponents方式已被弃用。苹果似乎建议现在专门使用NSDateFormatters。它们使得与任何组件轻松搭配变得非常容易。还请参阅Gilean的答案。 - andreb
1
@andreb NSCalendarDate 已经被弃用,但 NSDateComponents 绝对没有。从 NSDate 获取这些组件的正确方法是使用 NSCalendarNSDateComponents,如此答案所示。NSDateFormatter 应该用于将日期转换为/从字符串表示形式,而不是获取单个日期组件。 - Nick Forge

14

我还不能编辑,但是我使用了Gilean的代码并进行了一些调整,将其转换为NSDateFormatter的一个类别。

它接受格式字符串,因此可以处理任意字符串,并添加了if语句以使单数事件在语法上正确。

谢谢,

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

8

为了完整起见,基于@Gilean的答案,这里是一个简单的NSDate类别的完整代码,它模拟了rails的漂亮日期助手。关于类别的复习,这些都是您将在NSDate对象上调用的实例方法。因此,如果我有一个表示昨天的NSDate,[myDate distanceOfTimeInWordsToNow] =>“1 day”。

希望对您有所帮助!

@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

哦,还有:我在这个实现中省略了“前”这个词,以使其更有用。所以你可以调用这个方法并说:“距离你上次忏悔已经过去了%@。”,[confessionDate distanceOfTimeInWordsToNow] - Chris Ladd

6

已经有许多答案提供了相同的解决方案,但是多一些选择也无妨。以下是我想出来的解决方法。

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

4
我采用Carl Coryell-Martin的代码,并制作了一个更简单的NSDate类别,它不会对单数形式的字符串格式化发出警告,并且还将一周前的单数形式整理好了。
@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

3

在Swift中

用法:

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

扩展:

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"
        }
    }
}

我认为这个方法相当不错。只是,时间转换难道不应该也是函数的一部分吗? - Antoine
你在谈论 getTimeData 吗? 我将它从类作用域中封装,并仅在函数中赋予它生命,因为在其他情况下它是无用的。 - dimpiax
不,我的意思是你在“使用”说明中的计算。这部分 NSDate(timeIntervalSince1970: timestamp).timeIntervalSinceNow * -1 不应该被添加到主函数 relativeTimeInString 中吗?因为现在你总是需要在那之前进行这个计算。 - Antoine
就像在独立情况下一样,你是正确的,需要封装这个转换。已更改 - dimpiax

1
使用NSDate类:
timeIntervalSinceDate

返回时间间隔(秒)。

快速练习,使用Objective-C实现:

  1. 获取当前时间NSDate
  2. 获取要比较的NSDate
  3. 使用timeIntervalSinceDate获取时间间隔(秒)

然后实现这个伪代码:

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

等等...

然后你需要决定一个月是30天、31天还是28天。保持简单 - 选择30天。

可能有更好的方法,但现在已经是凌晨2点了,这是我想到的第一件事...


1

我的解决方案:

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

0

不确定为什么这不在cocoa-touch中,一个好的标准方法会很棒。

设置一些类型来保持数据,如果您需要更多本地化,则会使其更容易。(如果需要更多时间段,请扩展)

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

注意:我使用基于小时的计算,因为我的数据是这样的。NSTimeInterval基于秒。我还需要在两者之间进行转换。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接