1

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
  • What happens if you use the @"UTC" time zone instead of @"GMT" ? They should be the same other than leap seconds, which I thought were ignored. I think as of now there have been 37 leap seconds, which would be just about half your discrepancy, so not sure that explains it unless it its being adjusted for twice (and once too many). But that seems weak. – Carl Lindberg Dec 14 '18 at 23:47
  • @CarlLindberg Just tested UTC, it prints the same thing as GMT (08:58:45). The NSLog for printing the timezone prints "GMT" instead of UTC. I believe behind the scenes UTC and GMT are the same. – sudoExclaimationExclaimation Dec 14 '18 at 23:51
  • They are technically different, but they should result in the same time being displayed. Just was making sure though. On the other hand, it does appear to be displaying for the year zero (1 BC). There could be some oddities from that. Maybe try setting the year in the NSDateComponents to something a bit more modern? – Carl Lindberg Dec 15 '18 at 00:01
  • @CarlLindberg if I set the components to `NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:[timeFormatter dateFromString:@"01-01-2000 9:00 AM"]` and the format to `@"MM-dd-yyyy h:mm a"` then it does seem to match. Why's it necessary though if I had originally asked the date component to only use hour and minute? – sudoExclaimationExclaimation Dec 15 '18 at 00:07
  • 2
    It's converting to NSDate, which is a fully-specified moment in time, so you need to supply a year / month / day value etc. to get an NSDate out of component values -- i.e. it always needs full calendrical conversions to create an NSDate, and it will use default values if none are specified (otherwise would have to return nil). And I guess there are subtle differences in the time zone implementations once you get to BC years. The bad part is that NSDateFormatter cannot operate directly on NSDateComponents, requiring the NSDate step, and you're best using current years for that. – Carl Lindberg Dec 15 '18 at 01:04

1 Answers1

4

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.

Carl Lindberg
  • 2,902
  • 18
  • 22
  • This is the most amazing and interesting answer I have seen to a question I asked on SO. Thank you so much for this, very well explained and makes sense! Just curious, how did you go across investigating this issue and come across the `This yields GMTOFF = -0:01:15 for London LMT in the 18th century.`? Like what was your flow which took you to that article? Thanks again! – sudoExclaimationExclaimation Dec 16 '18 at 06:12
  • 1
    When I tried locally, and saw the different (and seemingly random) results with America/New_York, it dawned on me what was going on. After that, it was downloading the tzdata files (available at the IANA link I gave in the answer) to see what the specific history was for Europe/London. The tzdata source files have copious comments on their stuff, and are constantly being updated. They can be interesting to read, if only to appreciate the effort it takes to keep up with changes in all corners of the world, and the whims of politicians. – Carl Lindberg Dec 16 '18 at 18:52
  • 1
    (An old favorite is the northamerica tzdata file, when it tries to track what happened when the recently-created Canadian territory Nunavut tried to go to a single time zone in 1999 -- there was some resistance to that, such as in the town of Pangnirtung, which resulted in a situation impossible to specify in the files. Over the next couple of years, Nunavut backtracked on the single time zone I believe, and went back to three.) – Carl Lindberg Dec 16 '18 at 19:14
  • Thank you for the explanation, I have you a +50 bounty! – sudoExclaimationExclaimation Dec 22 '18 at 07:43