2

This should be a really simple question but I just can't seem to wrap my head around it.

Given my timezone is EDT (GMT-4), why does 04:00 in GMT turn into 23:00 and not 00:00?

// The offset is -4 hours
let offsetFromGMT = Calendar.current.timeZone.secondsFromGMT() / 60 / 60

// 2017-03-12 04:00
var destinationComponents = DateComponents()
destinationComponents.timeZone = TimeZone(secondsFromGMT: 0)
destinationComponents.year = 2017
destinationComponents.month = 03
destinationComponents.day = 12
destinationComponents.hour = -offsetFromGMT // 4 hours

// Why is this 2017-03-11 23:00 and not 2017-03-12 00:00?
let date = Calendar.current.date(from: destinationComponents)!
// Outputs 23
Calendar.current.dateComponents([.hour], from: date).hour
Robert Gummesson
  • 5,630
  • 2
  • 17
  • 16
  • `print(date)` should show `2017-03-12 04:00:00 +0000`. – Depending on whether you have daylight saving time active or not at that date at your location, that may be `2017-03-12 00:00` or `2017-03-11 23:00` in your local timezone. – What does `Calendar.current.timeZone.identifier` print? – Martin R Mar 13 '17 at 10:36
  • It prints "America/New_York" and print(date) shows 2017-03-12 04:00:00 +0000. Yes, but why is the daylight savings not included in secondsFromGMT() in that case? – Robert Gummesson Mar 13 '17 at 10:42
  • `secondsFromGMT()` is the current GMT offset. There is another function `secondsFromGMT(for: date)` which returns the GMT offset for the specified date, that would include DST. – What are you actually trying to achieve? – Martin R Mar 13 '17 at 10:47
  • Yes, I've seen that one but if I do Calendar.current.timeZone.secondsFromGMT(for: Date()), it's still -14400 (-4 hours). It's -18000 if I pass in date instead of Date() though which makes little sense to me. For background, I have an app that works with GMT dates but somewhere the translation from EDT to GMT goes wrong. I'm trying to debug the code when I noticed this. The sample code above just illustrates the problem. – Robert Gummesson Mar 13 '17 at 11:00

1 Answers1

8
Calendar.current.timeZone.secondsFromGMT()

is the current GMT offset for your time zone. In your case that is 4 hours, because the current time zone in New York is EDT = GMT-4, with daylight saving time active.

So your destinationComponents and date are four o'clock in the morning Greenwich time:

2017-03-12 04:00:00 +0000

At that point, the time zone in New York was EST = GMT-5, and daylight saving time not active. Therefore that date is 2017-03-11 23:00 in your local time zone.


I would proceed differently, avoiding "secondsFromGMT".

Example: "2017-03-12 00:00:00" New York time is "2017-03-12 05:00:00" GMT.

var srcComponents = DateComponents()
srcComponents.timeZone = TimeZone(identifier: "America/New_York")!
srcComponents.year = 2017
srcComponents.month = 3
srcComponents.day = 12
srcComponents.hour = 0
srcComponents.minute = 0

let date = Calendar.current.date(from: srcComponents)!
print(date) // 2017-03-12 05:00:00 +0000
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Ah, I didn't actually realise daylight savings time has just changed. I was under the impression that would be later during the year. Thanks. That explains everything. – Robert Gummesson Mar 13 '17 at 11:08
  • 1
    @Robert: You are welcome! – I have added another approach which might be useful. – Martin R Mar 13 '17 at 11:14
  • 1
    One would think `secondsFromGMT`, a math computation, is safer than an untyped string literal but that is not the case because `secondsFromGMT` doesn't account for half-hour time zones (there are 4). I'd recommend using `abbreviation` over `identifier` because abbreviation is a "standardized" list (also much shorter than the identifier list) and because I couldn't find India or Newfoundland on the identifier list (2 of the 4 half-hour time zones) even though I'm sure (I hope) they're buried somewhere in that long list. There are about 20 Indianas and Indian reservations, but no Indias. – trndjc Nov 06 '18 at 16:29
  • For completeness, there are also 45-minute time zones but I haven't found a way, or frankly even a desire, to target them using any approach. There are about 35-40 "recognized" UTC zone designations in the world and hardly anybody is using the same list. To the countries with 45-minute time zones, I say sorry, you don't exist to me. When I say 30-minute and 45-minute, I am referring to their offset from GMT, not the span of the time zone itself. – trndjc Nov 06 '18 at 16:38
  • @narddog: Is the abbreviation really standardized? According to https://stackoverflow.com/a/18407231/1187415, it can be ambiguous: *"For example, CST could be "Central Standard Time" (USA), "China Standard Time", or "Cuba Standard Time"."* – Martin R Nov 06 '18 at 18:28
  • @MartinR That's why I put standardized in quotes. `PST` is more standardized than `"America/Indiana/Winamac"`. When is Apple going to stop supporting `"America/Indiana/Winamac"` in favor of `"America/Indiana/Vevay"` or `"America/Indiana/Tell_City"`. More I think about it, the approach I'd probably favor is a hybrid one that gets all "integer" time zones (offsets with just hours and no minutes) by `secondsFromGMT` (zero failure) and the fractional time zones by the safest abbreviation first (i.e. IRST; not changing) and default to identifier as a last resort (i.e. "Pacific/Chatham" for +12:45). – trndjc Nov 06 '18 at 18:56
  • @narddog: But secondsFromGMT does not take the DST into account. "Europe/Berlin" can be GMT+1 or GMT+2. – Martin R Nov 06 '18 at 19:01
  • @MartinR That's a good point and that really poses a problem when dealing with literal UTC time zones (i.e. +02:00). You would have no idea if the territory in the zone observers DST (not even all states in the US observe it). I was assuming (for whatever reason) that the solution called for handling random UTC strings where there was no way of knowing if -07:00 is Phoenix (which doesn't observe) or Denver (which does). Your solution is, then, most favorable when that context is known. And when it's not known, god help us. – trndjc Nov 06 '18 at 19:13