两个NSDates之间的天数

154

我如何确定两个NSDate值之间的天数(同时考虑时间)?

NSDate值以[NSDate date]的形式呈现。

具体地说,当用户在我的iPhone应用程序中进入非活动状态时,我存储以下数值:

exitDate = [NSDate date];

当他们重新打开应用程序时,我获取当前时间:

NSDate *now = [NSDate date];

现在我想要实现以下内容:

-(int)numberOfDaysBetweenStartDate:exitDate andEndDate:now
16个回答

414

下面是我使用的一个实现方法,用于确定两个日期之间的日历天数:

+ (NSInteger)daysBetweenDate:(NSDate*)fromDateTime andDate:(NSDate*)toDateTime
{
    NSDate *fromDate;
    NSDate *toDate;

    NSCalendar *calendar = [NSCalendar currentCalendar];

    [calendar rangeOfUnit:NSCalendarUnitDay startDate:&fromDate
        interval:NULL forDate:fromDateTime];
    [calendar rangeOfUnit:NSCalendarUnitDay startDate:&toDate
        interval:NULL forDate:toDateTime];

    NSDateComponents *difference = [calendar components:NSCalendarUnitDay
        fromDate:fromDate toDate:toDate options:0];

    return [difference day];
}

编辑:

上面提供了一种精彩的解决方案,这里是Swift版本,作为NSDate的扩展:

extension NSDate {
  func numberOfDaysUntilDateTime(toDateTime: NSDate, inTimeZone timeZone: NSTimeZone? = nil) -> Int {
    let calendar = NSCalendar.currentCalendar()
    if let timeZone = timeZone {
      calendar.timeZone = timeZone
    }

    var fromDate: NSDate?, toDate: NSDate?

    calendar.rangeOfUnit(.Day, startDate: &fromDate, interval: nil, forDate: self)
    calendar.rangeOfUnit(.Day, startDate: &toDate, interval: nil, forDate: toDateTime)

    let difference = calendar.components(.Day, fromDate: fromDate!, toDate: toDate!, options: [])
    return difference.day
  }
}

如果根据使用情况需要的话,你可能需要删除一些强制解包操作。

上述解决方案也适用于除当前时区以外的时区,非常适合应用程序显示世界各地地点信息的场景。


7
为什么不只使用NSDateComponents *difference = [calendar components:NSDayCalendarUnit fromDate:fromDate toDate:toDate options:0];? - karim
12
如果你只使用那个函数,时间差别不会以“日历天”为单位。 - João Portela
40
为了举例说明João所说的内容,假设fromDateTime = Feb 1st 11:00pm,toDateTime = Feb 2nd 01:00am,那么结果应该是1(即使只有两小时,但是它跨越了两个日期)。如果不剥离日期的时间部分(这是通过调用rangeOfUnit:...来完成的),结果将为0(因为2h < 1 day)。 - Daniel Rinser
1
Karim - 很好的解决方案,这正是我在寻找的。 - Richard Topchii
4
在“rangeOfUnit”方法之前,我添加了“[calendar setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];”,现在它对我来说正常工作了(运行环境的本地时区设置为+4:30 GMT)! - Majid
显示剩余8条评论

118

这是我找到的最佳解决方案。似乎利用了苹果批准的方法来确定两个NSDate之间的任意单位数量。

- (NSInteger)daysBetween:(NSDate *)dt1 and:(NSDate *)dt2
{
    NSUInteger unitFlags = NSCalendarUnitDay;
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSDateComponents *components = [calendar components:unitFlags fromDate:dt1 toDate:dt2 options:0];
    return [components day] + 1;
}
例如,如果您还想获取月份,则可以将“NSMonthCalendarUnit”作为unitFlag包含在内。
为了归功于原博主,我在这里找到了这些信息(尽管上面有一个小错误,我已经修复了): http://cocoamatic.blogspot.com/2010/09/nsdate-number-of-days-between-two-dates.html?showComment=1306198273659#c6501446329564880344

8
这是苹果文档中的方法。这应该被接受。 - Jonathan.
1
日历的声明在哪里?为什么不将其实现为一个类别? - Rob
3
应该将 return [components day]+1; 改为 return ABS([components day])+1;,以防止负值时计算错误的天数。 - Tek Yin
5
它的工作不正确。对于相同的日期,以及相差1天的日期,它都会给出相同的结果。在这种情况下,components.day为0。 - Shmidt
2
我想要一个方法,对于过去的toDate返回负值,对于未来的toDate返回正值,如果toDate与fromDate是同一天,则返回0。如果我删除代码最后一行中的+1,这个方法就可以实现我的需求。 - Xander Dunn
显示剩余8条评论

25

Swift 3.0 更新

extension Date {

    func differenceInDaysWithDate(date: Date) -> Int {
        let calendar = Calendar.current

        let date1 = calendar.startOfDay(for: self)
        let date2 = calendar.startOfDay(for: date)

        let components = calendar.dateComponents([.day], from: date1, to: date2)
        return components.day ?? 0
    }
}

Swift 2.0 更新

extension NSDate {

    func differenceInDaysWithDate(date: NSDate) -> Int {
        let calendar: NSCalendar = NSCalendar.currentCalendar()

        let date1 = calendar.startOfDayForDate(self)
        let date2 = calendar.startOfDayForDate(date)

        let components = calendar.components(.Day, fromDate: date1, toDate: date2, options: [])
        return components.day
    }

}

原始解决方案

以下是Swift中的另一种解决方案。

如果您的目的是获取两个日期之间的确切天数,您可以像这样解决此问题:

// Assuming that firstDate and secondDate are defined
// ...

var calendar: NSCalendar = NSCalendar.currentCalendar()

// Replace the hour (time) of both dates with 00:00
let date1 = calendar.startOfDayForDate(firstDate)
let date2 = calendar.startOfDayForDate(secondDate)

let flags = NSCalendarUnit.DayCalendarUnit
let components = calendar.components(flags, fromDate: date1, toDate: date2, options: nil)

components.day  // This will return the number of day(s) between dates

1
比使用rangeOfUnit更简短、更简洁。但是(目前)需要注意的是,这需要iOS 8。 - nschum
1
只有这个答案对我有用!+1 为 calendar.startOfDayForDate。谢谢。 - Thomás Pereira
我认为你需要在日历上设置一个时区,否则设备的时区将被使用,这可能会影响结果。 - Leon
@Leon 这就是问题所在。你不能只改变其中一个日期。如果日期有变化,应该同时应用于两个日期。这样时区在这种情况下就不会成为问题。 - Emin Bugra Saral
@EminBuğraSaral 不,你误解了,我正在更改一个日期以进行测试。实际上,我无法控制时间,因为它们来自作为UTC时间戳的API。我需要能够计算两个日期之间的日历天数,但当时间戳从<2300更改为2300及以上时,我的测试设备返回了两个不同的结果,这是错误的。 - Leon
显示剩余2条评论

13

我使用这个作为NSDate类的分类方法。

// returns number of days (absolute value) from another date (as number of midnights beween these dates)
- (int)daysFromDate:(NSDate *)pDate {
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSInteger startDay=[calendar ordinalityOfUnit:NSCalendarUnitDay
                                           inUnit:NSCalendarUnitEra
                                          forDate:[NSDate date]];
    NSInteger endDay=[calendar ordinalityOfUnit:NSCalendarUnitDay
                                         inUnit:NSCalendarUnitEra
                                        forDate:pDate];
    return abs(endDay-startDay);
}

2
这是最好的且能正确运作的解决方案。 - Shmidt
1
你为什么讨厌currentCalendar? - pronebird
这段内容引用自苹果文档 https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/DatesAndTimes/Articles/dtCalendricalCalculations.html - Jonathan Mitchell
因为这种方法在某些情况下可能会返回不正确的差异,因为它不包括时区偏移量,所以被踩了。 - Vitalii Gozhenko
也被踩了,因为这种方法在某些情况下可能会返回不正确的差异,因为它不包括时区偏移。 - Krodak

8

我需要计算两个日期之间的天数,包括开始那一天。 例如,2012年2月14日至2012年2月16日之间的天数应该是3。

+ (NSInteger)daysBetween:(NSDate *)dt1 and:(NSDate *)dt2 {
        NSUInteger unitFlags = NSDayCalendarUnit;
        NSCalendar* calendar = [NSCalendar currentCalendar];
        NSDateComponents *components = [calendar components:unitFlags fromDate:dt1 toDate:dt2 options:0];
        NSInteger daysBetween = abs([components day]);
    return daysBetween+1;
}

请注意,提供日期的顺序不重要。它总是返回一个正数。

7
NSDate *lastDate = [NSDate date];
NSDate *todaysDate = [NSDate date];
NSTimeInterval lastDiff = [lastDate timeIntervalSinceNow];
NSTimeInterval todaysDiff = [todaysDate timeIntervalSinceNow];
NSTimeInterval dateDiff = lastDiff - todaysDiff;

dateDiff将是两个日期之间的秒数。只需除以一天中的秒数即可。


2
为了清晰起见,您需要除以606024,然后需要决定部分天是否算作一天,5.9是否等于6天?找出适合您应用程序的适当舍入方案。 - Chris Wagner
71
在应用程序中,你绝不应该进行这种数学计算。这样做会随着时区转换而出现问题(可能导致23或25小时的一天),跳秒(在某些年份末应用但在其他年份不应用),以及任何其他日历复杂性。 - Becca Royal-Gordon
8
NSCalendar具有执行日期数学运算的方法。WWDC 2011会议视频包括一个关于如何安全地进行日期数学运算的会话。 - Becca Royal-Gordon
7
回答错误,请参考Biosopher的回答。 - nont
6
这已经远远超出了错误的范畴,毫不好笑。Biosopher的回答是正确的。 - Tony Million
显示剩余4条评论

5

@Brian

虽然Brian的答案不错,但只计算24小时块的天数差异,而不是日历天数差异。例如,在12月24日23:59离圣诞节仅有1分钟,对于许多应用程序来说,仍然被认为是同一天。Brian的daysBetween函数将返回0。

借鉴Brian原始实现和一天的开始/结束,我在我的程序中使用以下内容:(NSDate beginning of day and end of day)

- (NSDate *)beginningOfDay:(NSDate *)date
{
    NSCalendar *cal = [NSCalendar currentCalendar];
    NSDateComponents *components = [cal components:( NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit ) fromDate:date];
    [components setHour:0];
    [components setMinute:0];
    [components setSecond:0];
    return [cal dateFromComponents:components];
}

- (NSDate *)endOfDay:(NSDate *)date
{
    NSCalendar *cal = [NSCalendar currentCalendar];
    NSDateComponents *components = [cal components:( NSDayCalendarUnit | NSMonthCalendarUnit | NSYearCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit ) fromDate:date];
    [components setHour:23];
    [components setMinute:59];
    [components setSecond:59];
    return [cal dateFromComponents:components];
}

- (int)daysBetween:(NSDate *)date1 and:(NSDate *)date2 {
    NSDate *beginningOfDate1 = [self beginningOfDay:date1];
    NSDate *endOfDate1 = [self endOfDay:date1];
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    NSDateComponents *beginningDayDiff = [calendar components:NSDayCalendarUnit fromDate:beginningOfDate1 toDate:date2 options:0];
    NSDateComponents *endDayDiff = [calendar components:NSDayCalendarUnit fromDate:endOfDate1 toDate:date2 options:0];
    if (beginningDayDiff.day > 0)
        return beginningDayDiff.day;
    else if (endDayDiff.day < 0)
        return endDayDiff.day;
    else {
        return 0;
    }
}

1
实际上,我的解决方案的整个目的是计算日历天数的差异 - 试一下吧 :-)。时间戳的时间部分被丢弃,因此只返回天数的差异(因此12月24日23:59:59和12月25日被认为相隔1天)。 - Brian

3

另一种方法:

NSDateFormatter* dayFmt = [[NSDateFormatter alloc] init];
[dayFmt setTimeZone:<whatever time zone you want>];
[dayFmt setDateFormat:@"g"];
NSInteger firstDay = [[dayFmt stringFromDate:firstDate] integerValue];    
NSInteger secondDay = [[dayFmt stringFromDate:secondDate] integerValue];
NSInteger difference = secondDay - firstDay;

拥有比`timeIntervalSince...`方案更大的优势,即可以考虑时区,并且不存在几秒钟短或长于一天的时间间隔不明确的问题。比NSDateComponents方法更加紧凑和清晰易懂。

3

这是Brian函数在Swift中的实现:

class func daysBetweenThisDate(fromDateTime:NSDate, andThisDate toDateTime:NSDate)->Int?{

    var fromDate:NSDate? = nil
    var toDate:NSDate? = nil

    let calendar = NSCalendar.currentCalendar()

    calendar.rangeOfUnit(NSCalendarUnit.DayCalendarUnit, startDate: &fromDate, interval: nil, forDate: fromDateTime)

    calendar.rangeOfUnit(NSCalendarUnit.DayCalendarUnit, startDate: &toDate, interval: nil, forDate: toDateTime)

    if let from = fromDate {

        if let to = toDate {

            let difference = calendar.components(NSCalendarUnit.DayCalendarUnit, fromDate: from, toDate: to, options: NSCalendarOptions.allZeros)

            return difference.day
        }
    }

    return nil
}

3

对于那些想在Swift中实现此操作的访问此页面的人,我帮你们添加一个答案。方法基本相同。

private class func getDaysBetweenDates(startDate:NSDate, endDate:NSDate) -> NSInteger {

    var gregorian: NSCalendar = NSCalendar.currentCalendar();
    let flags = NSCalendarUnit.DayCalendarUnit
    let components = gregorian.components(flags, fromDate: startDate, toDate: endDate, options: nil)

    return components.day
}

这个答案可以在以下方法的讨论部分找到

components(_:fromDate:toDate:options:)

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