6

Am I missing something here? It seems like the method provided by Apple only works for UTC, regardless of the timezone default of the machine, or what you set it to.

Here's the output I get:

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

What I really want to know is "how many midnights" (i.e., how many calendar days diff) between two days for a given timezone (local or otherwise, and not necessarily UTC)

This seems like such a common and reasonably simple question that I'm surprised to see how messy and difficult to figure out.

I'm not looking for an answer that involves "mod 86400" or something filthy like that. The framework should be able to tell me this, seriously.

- (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];
}
lnafziger
  • 25,760
  • 8
  • 60
  • 101
jpswain
  • 14,642
  • 8
  • 58
  • 63
  • 1
    possible duplicate of [Number of days between two NSDates](http://stackoverflow.com/questions/4739483/number-of-days-between-two-nsdates). Make sure that you use the right answer though: http://stackoverflow.com/a/4739650/937822 and use your same calendar instead of the one that his function creates. – lnafziger Feb 02 '13 at 05:12
  • @lnafziger No, it is not a duplicate as far as I can tell. I searched extensively for such an answer before posting. I'd obviously rather not type all that up if I can help it. The whole reason i asked the question is b/c answers like the one you posted DO NOT work correctly for all timezones. I looked through all the answers there and none of them appear solve this problem. – jpswain Mar 07 '13 at 09:02
  • 1
    "As far as I can tell"? Did you try it? I did. It works exactly as you asked. "The number of midnights between two dates in a given timezone" IF, as I said in my original comment, you use your same calendar instead of the one in that function. Again, use stackoverflow.com/a/4739650/937822 as the base for your function, replacing the default calendar that is used with the one that you want to use, and it works perfectly. If I am missing something, then by all means tell me what doesn't work and I'd be happy to look into it as I know the date/time methods of iOS quite well. – lnafziger Mar 07 '13 at 22:12
  • Do you think we should keep this question open b/c it will be relevant to any one having the same problem with timezones? For someone who stumbles upon the other question it is not at all obvious that the one answer out of many listed is the only one that actually works correctly with timezones, especially because it is not the chosen answer. – jpswain Mar 09 '13 at 21:17
  • 1
    Well, just because a question is closed doesn't mean that it goes away. In fact, SO encourages people to leave closed questions around (rather than delete or merge them) as "sign posts" for people looking for a particular answer. Besides, I wouldn't worry about it: whether the question is closed or not isn't up to you or I, but the community. They will vote as they see fit and the question of whether or not to close it will be resolved on its own. In the end, I'm just happy that you found your answer and that's why we are here in the first place! – lnafziger Mar 09 '13 at 23:10

6 Answers6

3

Using this answer as a starting point, I simply added the calendar as an additional argument (you could just pass a timezone instead of the calendar if you want):

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

This will use the calendar that you pass to determine the number of midnights between fromDateTime and toDateTime and return it as a NSInteger. Make sure that you set the timezone appropriately.

Using your examples above, this is the output:

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

So yes, I stand by my voting to close this question as a duplicate as it is VERY close to what you are doing. :-)

Community
  • 1
  • 1
lnafziger
  • 25,760
  • 8
  • 60
  • 101
  • OK yes that works. Sorry about that. I thought yours was the same as the apple answer here: (Listing 13) http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/DatesAndTimes/Articles/dtCalendricalCalculations.html#//apple_ref/doc/uid/TP40007836-SW1 Please notice that they are *both* using calendar-based methods and day unit. It still seems strange to me that theirs doesn't work right and yours does. Again sorry for the mix up. Maybe you should write a headline on your answer that "it works regardless of TZ unlike most other answers." That would be helpful. Thanks. – jpswain Mar 08 '13 at 07:50
  • @patric.schenke posted the same solution previously, so I'll select his as the solution. I that case too I thought it was the same as the Apple one I mentioned. – jpswain Mar 08 '13 at 07:55
  • 2
    Yeah, their code uses a different function (`ordinalityOfUnit:inUnit:forDate:` instead of `components:fromDate:toDate:`) to make the calculation. I would think that their method should work too, but it appears as if it is not taking the time zone into consideration as it should. We should report this behavoir as a bug and see what they come back with. – lnafziger Mar 09 '13 at 01:33
  • 1
    Yeah it seems pretty clear that it's a bug... any calculation of dates using a calendar method should take into account the calendar's time zone. – jpswain Mar 09 '13 at 07:55
2

Here's how I'd go about it:

// 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);
Dave DeLong
  • 242,470
  • 58
  • 448
  • 498
  • Hey thanks for the answer man. I ended up going with what I posted as an alternate answer here b/c it seemed a little cleaner and left more in the hands for the framework (as opposed to manually looping over all the days). I'm giving yours +1 though b/c it definitely appears to work based the tests I did. – jpswain Mar 04 '13 at 17:49
1

I've written a small category to find the number of days between two NSDate-objects. If I understand your question correctly, that's what you want.

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
patric.schenke
  • 942
  • 11
  • 19
  • 1
    Note to others trying to use this solution: You will need to replace `cal` with the appropriate calendar that you want to use (since the original question wants to do this with an "arbitrary" time zone/calendar). This would best be accomplished by adding either a time zone or a calendar as another argument to this method and using that instead of `cal`. – lnafziger Mar 09 '13 at 01:39
1

This is the best I was able to come up with.

It works regardless of what the current calendar is and time of day.

- (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;
}
jpswain
  • 14,642
  • 8
  • 58
  • 63
0

This seems to work well:

Here is a category to find the number of midnights between two NSDate-objects.

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
Filip Duvnjak
  • 51
  • 1
  • 2
-1

Heres a Swift 3 extension to Calendar that will do the trick

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