Search code examples
iosobjective-cnsdateformatternscalendarnstimezone

How to obtain NSCalendarComponent values relative to time zone other than GMT?


Why do the following marked assertions fail? I was just running this unit test on a host in Central Europe. Hence NSCalendar.currentCalendar.timeZone is CEST, i.e. GMT+0200. NSDateComponents returns this time zone but its other values (for year, etc.) are apparently relative to GMT. How can I obtain values that are relative to CEST?

- (void)test {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mmZZZ";
    dateFormatter.timeZone   = [NSTimeZone timeZoneWithAbbreviation:@"CEST"];
    XCTAssertEqual(2 * 60 * 60, dateFormatter.timeZone.secondsFromGMT, @"");

    NSDate *time = [dateFormatter dateFromString:@"2014-01-01T00:00+0200"]; // midnight on a Wednesday

    NSCalendar *calendar = NSCalendar.currentCalendar; // i.e. CEST
    XCTAssertEqual(2 * 60 * 60, calendar.timeZone.secondsFromGMT, @"");

    NSDateComponents *components = [calendar components: NSYearCalendarUnit |
                                                         NSMonthCalendarUnit |
                                                         NSDayCalendarUnit |
                                                         NSWeekdayCalendarUnit |
                                                         NSHourCalendarUnit |
                                                         NSMinuteCalendarUnit |
                                                         NSTimeZoneCalendarUnit
                                               fromDate:time];

    XCTAssertEqual(components.year, 2014, @""); // fails with 2013
    XCTAssertEqual(components.month, 1, @""); // fails with 12
    XCTAssertEqual(components.day, 1, @""); // fails with 31
    XCTAssertEqual(components.weekday, 4, @""); // fails with 3 (Tuesday)
    XCTAssertEqual(components.hour, 0, @""); // fails with 23
    XCTAssertEqual(components.minute, 0, @""); // succeeds
    XCTAssertEqual(components.timeZone.secondsFromGMT, 2 * 60 * 60, @""); // succeeds (CEST)
}

Solution

  • NSCalendar.currentCalendar.timeZone is CEST or GMT+02 now because Daylight Saving Time is active now in your time zone. But on 2014-01-01, Daylight Saving Time was not active. Therefore all conversions for that date are done with GMT+01 offset.

    So here is what happens in your case:

    • The string "2014-01-01T00:00+0200" is converted to the NSDate "2013-12-31 22:00:00 +0000", because you explicitly specify the GMT offset "+0200" in the input string. Setting dateFormatter.timeZone to "CEST" has therefore no effect.

    • The NSDate "2013-12-31 22:00:00+0000" is converted to date components, using your current calendar. Since the GMT offset for that date is "GMT+0100", you get the date components corresponding to "2013-12-31 23:00:00+0100".

    If you do the calculations with

    NSDate *time = [dateFormatter dateFromString:@"2014-01-01T00:00+0100"];
    

    then the test succeeds.