Search code examples
iosnsdatensdateformatternscalendarnsdatecomponents

Difference between setting NSCalendar's timezone using `localTimeZone` vs using `timeZoneWithAbbreviation` giving different values


I have configured my phone (actual device, not simulator) to be in London UK which is GMT.

When I set the timezone on a NSCalendar using localTimeZone, it gives me the time off by 1 minute and 15 seconds as compared to if I set it using [NSTimeZone timeZoneWithAbbreviation:@"GMT"]. Shouldn't both be giving me the same result if my timezone is already set to GMT?

Another thing I notice is that if I set break points, the NSDate created using localTimeZone inspected via the breakpoint is off by 1 minute and 15 seconds but the NSLog prints the correct time (Breakpoint shows: 0000-12-30 09:01:15 UTC but NSLog prints Sat Dec 30 09:00:00 0000):

enter image description here

But when I use [NSTimeZone timeZoneWithAbbreviation:@"GMT"], then the breakpoint shows the correct time but the NSLog prints the wrong time off by 1 minute and 15 seconds (Breakpoint shows: 0000-12-30 09:00:00 UTC but NSLog prints Sat Dec 30 08:58:45 0000):

enter image description here

Here's my code along with the breakpoint and NSLog output in comments next to it:

NSLog(@"Timezone: %@",[NSTimeZone localTimeZone]);//Timezone: Local Time Zone (Europe/London (GMT) offset 0)

    NSLocale *locale = [NSLocale currentLocale];
    NSCalendar *myCalendar = [NSCalendar currentCalendar];
    [myCalendar setLocale:locale];
    [myCalendar setTimeZone:[NSTimeZone localTimeZone]];
    NSLog(@"TZ: %@",myCalendar.timeZone);//TZ: Local Time Zone (Europe/London (GMT) offset 0)

    NSDateFormatter *timeFormatter = [NSDateFormatter new];
    [timeFormatter setDateFormat:@"h:mm a"];
    [timeFormatter setLocale:locale];
    [timeFormatter setTimeZone:[NSTimeZone localTimeZone]];

    NSDateComponents* dateComps = [myCalendar components:NSCalendarUnitHour|NSCalendarUnitMinute fromDate:[timeFormatter dateFromString:@"9:00 AM"]];
    NSDate *myDate1 = [myCalendar dateFromComponents:dateComps];//Breakpoint shows: 0000-12-30 09:01:15 UTC


    NSLog(@"myDate1: %@",myDate1); // myDate1: Sat Dec 30 09:00:00 0000

    //----------------Explicitly specified timeZoneWithAbbreviation GMT:


    NSCalendar *myCalendarExplicitGMT = [NSCalendar currentCalendar];
    [myCalendarExplicitGMT setLocale:locale];
    [myCalendarExplicitGMT setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]];
    NSLog(@"TZ: %@",myCalendarExplicitGMT.timeZone);//TZ: GMT (GMT) offset 0

    NSDateComponents* dateCompsUsingExplicitGMT = [myCalendarExplicitGMT components:NSCalendarUnitHour|NSCalendarUnitMinute fromDate:[timeFormatter dateFromString:@"9:00 AM"]];
    NSDate *myDate2 = [myCalendarExplicitGMT dateFromComponents:dateCompsUsingExplicitGMT];//Breakpoint shows: 0000-12-30 09:00:00 UTC


    NSLog(@"myDate2: %@",myDate2); // myDate2: Sat Dec 30 08:58:45 0000

Solution

  • During the calculations, you are getting an NSDateComponents with just hour and minute values. You are fine to that point. However, to use an NSDateFormatter with it, you are then converting to an NSDate (since that is what NSDateFormatter requires). An NSDate is an absolute instant in time, so it needs a fully-specified date to convert. Rather than returning nil, it is using default (zero) values for any fields missing in the NSDateComponents. Thus, it is using the year 1 (and actually adjusting backwards, into 1 BC). You see the year printed as "0000" when you do that.

    When you use an explicit GMT time zone, you are explicitly specifying the offsets, and it works basically as expected. However when NSDate is formatted using the local time zone, i.e. Europe/London for you, it will apply any historical variations which happened in that time zone since that is what "Europe/London" says to do. The IANA time zone system, which NSTimeZone uses, has a fairly complete history of all time zone changes going back to the creation of the time zone system itself. They ignore time zones which came and went before 1970 or so, but they try to track the history of the time zones they do have back to their origins. For London, it appears that they first used GMT back in 1847 as sort of a standard time (before the full time zone system was created). Before that though, they (and everyone else) used "Local Mean Time", which was the natural time at that town's location -- so for a town a degree of longitude away, their time would be four minutes different (1440 minutes in a day, divided by 360 degrees of longitude for the Earth, gets four minutes per degree). So, before 1847, they used "London Mean Time", which was slightly different than "Greenwich Mean Time". Looking at the tzdata "europe" source file, they give this description which they found in a 1994 Independent article:

    'An old stone obelisk marking a forgotten terrestrial meridian stands
    beside the river at Kew. In the 18th century, before time and longitude
    was standardised by the Royal Observatory in Greenwich, scholars observed
    this stone and the movement of stars from Kew Observatory nearby. They
    made their calculations and set the time for the Horse Guards and Parliament,
    but now the stone is obscured by scrubwood and can only be seen by walking
    along the towpath within a few yards of it.'
    
    I have a one inch to one mile map of London and my estimate of the stone's
    position is 51° 28' 30" N, 0° 18' 45" W. The longitude should
    be within about ±2". The Ordnance Survey grid reference is TQ172761.
    
    [This yields GMTOFF = -0:01:15 for London LMT in the 18th century.]
    

    So, they calculate before 1847, London (and thus the "Europe/London" time zone) was one minute and fifteen seconds offset from GMT, and it will adjust your time accordingly. Since your NSDate is for the year 1 BC, it is most definitely well before 1847, so you get that adjustment. When I tried America/New_York the same way, it was 3:58 different than pure GMT-5.

    So the short answer is -- when you are doing date conversions using modern time zones, you are best served keeping the rest of the date components modern as well. For example if you let the year drift back to WWII, you would start applying the double daylight savings time London had during the war, and from the 1800s and earlier you start getting into the time system which drove the railroad companies crazy ;-). Your other option, as you saw, is to use NSTimeZones which are not historical but have a predictable offset. The NSLog() though uses the NSDate -description method, which will in turn use the current (historical) time zone, and will always get those fairly arbitrary offsets for ancient years.