NSDate 比较,找出特定(本地)时区的“午夜数”

Posted

技术标签:

【中文标题】NSDate 比较,找出特定(本地)时区的“午夜数”【英文标题】:NSDate comparison, figuring out "number of midnights between" in specific (local) timezone 【发布时间】:2013-02-01 18:52:18 【问题描述】:

我在这里遗漏了什么吗?似乎 Apple 提供的方法仅适用于 UTC,无论机器的默认时区如何,或者您将其设置为什么。

这是我得到的输出:

Output:
2013-02-01 10:41:24.152 Scratch[17640:c07] cal=gregorian, cal.timeZone=America/Los_Angeles (PST) offset -28800
2013-02-01 10:41:24.154 Scratch[17640:c07] date_Feb1_1400PST=2013-02-01 14:00 -0800
2013-02-01 10:41:24.156 Scratch[17640:c07] date_Feb2_1200PST=2013-02-02 12:00 -0800
2013-02-01 10:41:24.157 Scratch[17640:c07] midnights between=1
2013-02-01 10:41:24.158 Scratch[17640:c07] and then...
2013-02-01 10:41:24.159 Scratch[17640:c07] date_Feb1_2000PST=2013-02-01 22:00 -0800
2013-02-01 10:41:24.161 Scratch[17640:c07] date_Feb2_1000PST=2013-02-02 10:00 -0800
2013-02-01 10:41:24.161 Scratch[17640:c07] midnights between=0

我真正想知道的是给定时区(本地或其他地方,不一定是 UTC )

这似乎是一个常见且相当简单的问题,我惊讶地发现它是多么混乱和难以弄清楚。

我不是在寻找涉及“mod 86400”或类似脏东西的答案。框架应该能够告诉我这一点,认真的。

- (void)doDateComparisonStuff 
    NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
    cal.timeZone = [NSTimeZone timeZoneWithName:@"America/Los_Angeles"];
    NSLog(@"cal=%@, cal.timeZone=%@", cal.calendarIdentifier, cal.timeZone);

    NSDate *date_Feb1_1400PST = [self dateFromStr:@"20130201 1400"];
    NSLog(@"date_Feb1_1400PST=%@", [self stringFromDate:date_Feb1_1400PST]);

    NSDate *date_Feb2_1200PST = [self dateFromStr:@"20130202 1200"];
    NSLog(@"date_Feb2_1200PST=%@", [self stringFromDate:date_Feb2_1200PST]);

    NSLog(@"midnights between=%d", [self daysWithinEraFromDate:date_Feb1_1400PST toDate:date_Feb2_1200PST usingCalendar:cal]);

    NSLog(@"and then...");

    NSDate *date_Feb1_2000PST = [self dateFromStr:@"20130201 2200"];
    NSLog(@"date_Feb1_2000PST=%@", [self stringFromDate:date_Feb1_2000PST]);

    NSDate *date_Feb2_1000PST = [self dateFromStr:@"20130202 1000"];
    NSLog(@"date_Feb2_1000PST=%@", [self stringFromDate:date_Feb2_1000PST]);

    NSLog(@"midnights between=%d", [self daysWithinEraFromDate:date_Feb1_2000PST toDate:date_Feb2_1000PST usingCalendar:cal]);


// based on "Listing 13" at
// https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/DatesAndTimes/Articles/dtCalendricalCalculations.html#//apple_ref/doc/uid/TP40007836-SW1
- (NSInteger)daysWithinEraFromDate:(NSDate *)startDate toDate:(NSDate *)endDate usingCalendar:(NSCalendar *)cal

    NSInteger startDay=[cal ordinalityOfUnit:NSDayCalendarUnit
                                       inUnit: NSEraCalendarUnit forDate:startDate];
    NSInteger endDay=[cal ordinalityOfUnit:NSDayCalendarUnit
                                     inUnit: NSEraCalendarUnit forDate:endDate];
    return endDay-startDay;



- (NSDate *)dateFromStr:(NSString *)dateStr 
    NSDateFormatter *df = nil;
    df = [[NSDateFormatter alloc] init];
    df.timeZone = [NSTimeZone timeZoneWithName:@"America/Los_Angeles"];
    df.dateFormat = @"yyyyMMdd HHmm";

    return [df dateFromString:dateStr];


- (NSString *)stringFromDate:(NSDate *)date 
    NSDateFormatter *df = nil;
    df = [[NSDateFormatter alloc] init];
    df.timeZone = [NSTimeZone timeZoneWithName:@"America/Los_Angeles"];  // native timezone here
    df.dateFormat = @"yyyy-MM-dd HH:mm Z";

    return [df stringFromDate:date];

【问题讨论】:

Number of days between two NSDates 的可能副本。但请确保您使用正确的答案:***.com/a/4739650/937822 并使用您的日历而不是他的函数创建的日历。 @lnafziger 不,据我所知,它不是重复的。在发布之前,我广泛搜索了这样的答案。如果我能提供帮助,我显然宁愿不输入所有内容。我问这个问题的全部原因是像你发布的那样的 b/c 答案在所有时区都不能正常工作。我查看了那里的所有答案,但似乎没有一个能解决这个问题。 “据我所知”?你试过了吗?我做到了。它完全按照您的要求工作。 “给定时区中两个日期之间的午夜数”如果,正如我在原始评论中所说,您使用相同的日历而不是该函数中的日历。同样,使用 ***.com/a/4739650/937822 作为您的函数的基础,将使用的默认日历替换为您要使用的日历,并且它可以完美运行。如果我遗漏了什么,那么一定要告诉我什么不起作用,我很乐意研究它,因为我非常了解 ios 的日期/时间方法。 你认为我们应该保持这个问题的开放,因为它与任何有相同时区问题的人有关吗?对于偶然发现另一个问题的人来说,列出的许多答案中的一个答案是唯一一个在时区中实际正确工作的答案并不明显,尤其是因为它不是选择的答案。 好吧,一个问题被关闭并不意味着它就消失了。事实上,SO 鼓励人们留下封闭的问题(而不是删除或合并它们)作为寻找特定答案的人的“标志帖子”。此外,我不会担心:问题是否关闭不取决于您或我,而是取决于社区。他们将在他们认为合适的时候投票,是否关闭它的问题将自行解决。最后,我很高兴您找到了答案,这就是我们最初来到这里的原因! 【参考方案1】:

使用this answer 作为起点,我只是添加了日历作为附加参数(如果需要,您可以只传递时区而不是日历):

- (NSInteger)daysBetweenDate:(NSDate*)fromDateTime andDate:(NSDate*)toDateTime usingCalendar:(NSCalendar *)calendar

    NSDate *fromDate;
    NSDate *toDate;

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

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

    return [difference day];

这将使用您传递的日历来确定fromDateTimetoDateTime 之间的午夜数,并将其作为NSInteger 返回。确保正确设置时区。

使用上面的示例,这是输出:

2013-03-07 17:23:54.619 Testing App[69968:11f03] cal=gregorian, cal.timeZone=America/Los_Angeles (PST) offset -28800
2013-03-07 17:23:54.621 Testing App[69968:11f03] date_Feb1_1400PST=20130201 1400
2013-03-07 17:23:54.621 Testing App[69968:11f03] date_Feb2_1200PST=20130202 1200
2013-03-07 17:23:54.622 Testing App[69968:11f03] midnights between=1
2013-03-07 17:23:54.622 Testing App[69968:11f03] and then...
2013-03-07 17:23:54.623 Testing App[69968:11f03] date_Feb1_2000PST=20130201 2200
2013-03-07 17:23:54.624 Testing App[69968:11f03] date_Feb2_1000PST=20130202 1000
2013-03-07 17:23:54.624 Testing App[69968:11f03] midnights between=1

所以是的,我支持以重复的方式结束这个问题,因为它与您正在做的事情非常接近。 :-)

【讨论】:

好的,可以。对于那个很抱歉。我认为您的答案与此处的苹果答案相同:(清单 13)developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/… 请注意,它们使用基于日历的方法和天单位。在我看来,他们的工作不正常而你的工作对我来说仍然很奇怪。再次为混淆感到抱歉。也许你应该在你的答案上写一个标题,“与大多数其他答案不同,它与 TZ 无关。”那会很有帮助。谢谢。 @patric.schenke 之前发布了相同的解决方案,所以我将选择他作为解决方案。我也是这种情况,我认为它与我提到的 Apple 相同。 是的,他们的代码使用不同的函数(ordinalityOfUnit:inUnit:forDate: 而不是components:fromDate:toDate:)进行计算。我认为他们的方法也应该有效,但似乎它没有考虑到应考虑的时区。我们应该将此行为报告为错误,并查看它们返回的结果。 是的,这似乎很明显这是一个错误...使用日历方法计算日期的任何计算都应考虑日历的时区。【参考方案2】:

下面是我的做法:

// pick a random timezone
// obviously you'd replace this with your own desired timeZone
NSArray *timeZoneNames = [NSTimeZone knownTimeZoneNames];
NSTimeZone *randomZone = [NSTimeZone timeZoneWithName:[timeZoneNames objectAtIndex:(arc4random() % [timeZoneNames count])]];

// create a copy of the current calendar
// (because you should consider the +currentCalendar to be immutable)
NSCalendar *calendar = [[NSCalendar currentCalendar] copy];

// change the timeZone of the calendar
// this causes all computations to be done relative to this timeZone
[calendar setTimeZone:randomZone];

// your start and end dates
// obviously you'd replace this with your own dates
NSDate *startDate = [NSDate dateWithTimeIntervalSinceReferenceDate:1234567890.0];
NSDate *endDate = [NSDate dateWithTimeIntervalSinceReferenceDate:1234890567.0];

// compute the midnight BEFORE the start date
NSDateComponents *midnightComponentsPriorToStartDate = [calendar components:NSEraCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit fromDate:startDate];
NSDate *midnightPriorToStartDate = [calendar dateFromComponents:midnightComponentsPriorToStartDate];

// this will keep track of how many midnights there are
NSUInteger numberOfMidnights = 0;

// loop F.O.R.E.V.E.R.
while (1) 
    // compute the nth midnight
    NSDateComponents *dayDiff = [[NSDateComponents alloc] init];
    [dayDiff setDay:numberOfMidnights+1];
    NSDate *nextMidnight = [calendar dateByAddingComponents:dayDiff toDate:midnightPriorToStartDate options:0];

    // if this midnight is after the end date, we stop looping
    if ([endDate laterDate:nextMidnight] == nextMidnight) 
        // this next midnight is after the end date
        break; // ok, maybe not forever
     else 
        // this midnight is between the start and end date
        numberOfMidnights++;
    


NSLog(@"There are %lu midnights between %@ and %@", numberOfMidnights, startDate, endDate);

【讨论】:

嘿,谢谢你的回答。我最终选择了我在这里发布的替代答案 b/c 它看起来更干净一些,并且为框架留下了更多的东西(而不是整天手动循环)。我给你的 +1 虽然 b/c 根据我所做的测试,它似乎确实有效。【参考方案3】:

我写了一个小类别来查找两个 NSDate 对象之间的天数。如果我正确理解你的问题,那就是你想要的。

NSDate+DaysDifference.h

#import <Foundation/Foundation.h>

@interface NSDate (DaysDifference)

- (NSInteger)differenceInDaysToDate:(NSDate *)otherDate;

@end

NSDate+DaysDifference.m

#import "NSDate+DaysDifference.h"

@implementation NSDate (DaysDifference)

- (NSInteger)differenceInDaysToDate:(NSDate *)otherDate 
    NSCalendar *cal = [NSCalendar autoupdatingCurrentCalendar];
    NSUInteger unit = NSDayCalendarUnit;
    NSDate *startDays, *endDays;

    [cal rangeOfUnit:unit startDate:&startDays interval:NULL forDate:self];
    [cal rangeOfUnit:unit startDate:&endDays interval:NULL forDate:otherDate];

    NSDateComponents *comp = [cal components:unit fromDate:startDays toDate:endDays options:0];
    return [comp day];


@end

【讨论】:

对尝试使用此解决方案的其他人的注意事项:您需要将 cal 替换为您要使用的适当日历(因为原始问题希望使用“任意”时区执行此操作/日历)。这最好通过添加时区或日历作为此方法的另一个参数并使用它而不是 cal 来完成。【参考方案4】:

这是我能想到的最好的。

无论当前日历是什么和一天中的时间,它都能正常工作。

- (NSInteger)daysApartFrom:(NSDate *)startDate toDate:(NSDate *)endDate usingCalendar:(NSCalendar *)localizedTZCal 

    static NSTimeZone *timeZoneUtc;
    static NSCalendar *utcCal;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^
        timeZoneUtc = [NSTimeZone timeZoneWithName:@"UTC"];

        utcCal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];        
        utcCal.timeZone = timeZoneUtc;
    );

    NSInteger componentFlags = NSEraCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
    NSDateComponents *componentsStartDate = [localizedTZCal components:componentFlags fromDate:startDate];
    NSDateComponents *componentsEndDate = [localizedTZCal components:componentFlags fromDate:endDate];

    NSDate *utcStartDate = [utcCal dateFromComponents:componentsStartDate];
    NSDate *utcEndDate = [utcCal dateFromComponents:componentsEndDate];

    NSInteger utcDaysDiff = [self daysWithinEraFromDate:utcStartDate toDate:utcEndDate usingCalendar:utcCal];
    return utcDaysDiff;


- (NSInteger)daysWithinEraFromDate:(NSDate *)startDate toDate:(NSDate *)endDate usingCalendar:(NSCalendar *)cal 
    NSInteger startDay = [cal ordinalityOfUnit:NSDayCalendarUnit inUnit: NSEraCalendarUnit forDate:startDate];
    NSInteger endDay = [cal ordinalityOfUnit:NSDayCalendarUnit inUnit:NSEraCalendarUnit forDate:endDate];

    return endDay - startDay;

【讨论】:

【参考方案5】:

这似乎运作良好:

这是一个查找两个 NSDate 对象之间的午夜数的类别。

NSDate+DaysDifference.h

#import <Foundation/Foundation.h>

@interface NSDate (DaysDifference)

+ (int) midnightsBetweenDate:(NSDate*)fromDateTime andDate:(NSDate*)toDateTime;

@end

NSDate+DaysDifference.m

#import "NSDate+DaysDifference.h"

@implementation NSDate (DaysDifference)

+ (int) midnightsBetweenDate:(NSDate*)fromDateTime andDate:(NSDate*)toDateTime;

      NSDate* sourceDate = [NSDate date];
      NSTimeZone* destinationTimeZone = [NSTimeZone systemTimeZone];
      NSInteger timeZoneOffset = [destinationTimeZone secondsFromGMTForDate:sourceDate];

      NSCalendar *calendar = [NSCalendar currentCalendar];
      NSInteger startDay = [calendar ordinalityOfUnit:NSDayCalendarUnit
                                               inUnit:NSEraCalendarUnit
                                             forDate:[fromDateTime dateByAddingTimeInterval:timeZoneOffset]];

      NSInteger endDay = [calendar ordinalityOfUnit:NSDayCalendarUnit
                                             inUnit:NSEraCalendarUnit
                                            forDate:[toDateTime dateByAddingTimeInterval:timeZoneOffset]];
      return endDay - startDay;


@end

【讨论】:

【参考方案6】:

这是日历的 Swift 3 扩展,可以解决问题

extension Calendar

    /**
     Calculates the number og midnights between two date-times
     - parameter firstDateTime: the date to start counting, defaults to today
     - parameter lastDateTime: the date to end counting
     - returns: the number of midnights between the given date-times
     - note: If firstDateTime is after lastDateTime the result may be negative
     */
    public func midnights(from firstDateTime: Date = Date(), until lastDateTime: Date) -> Int
    
        let firstStartOfDay = startOfDay(for: firstDateTime)
        let lastStartOfDay = startOfDay(for: lastDateTime)

        return Int(lastStartOfDay.timeIntervalSince(firstStartOfDay)/(60*60*24))
    

【讨论】:

以上是关于NSDate 比较,找出特定(本地)时区的“午夜数”的主要内容,如果未能解决你的问题,请参考以下文章

创建具有特定时区的 NSDate 对象

iOS:将 UTC NSDate 转换为本地时区

将 UTC 日期格式转换为本地 nsdate

OC - 时间日期类NSDate

将UTC日期格式转换为本地nsdate

NSDate: 我只输入utc时间成分,并正确设置日历,但NSDate不知为何会被重新计算到我的时区。