How can I find all the historical offset transition dates for a timezone?

Viewed 75

I'm trying to write an app that displays information about a given timezone. It displays which periods of time that timezone is observing DST, and which periods of time it is not. Most importantly, it highlights times where the timezone changes irregularly, such as when Britain observed double DST during WWII, or when Samoa skipped a day in 2011.

For that, I would need to get a list of all the historical timezone offset transitions, as stored in the TZ database (I think there is a copy of the database in every macOS/iOS device). To be more specific, a "transition" (similar to java.time.zone.ZoneOffsetTransition) is modelled by the following 3 things:

  • the Date when it happened
  • the GMT offset in seconds before it happened
  • the GMT offset in seconds after it happened

From what I can see from the TimeZone API docs, there is no built-in method that does this (unlike how java.time does). The closest method I could find is nextDaylightSavingTimeTransition(after:), but that only tells me the transition date of one transition, when given a date, and I'm also not sure what date to give.

How can I get the list of transitions?

Example output for Asia/Ho_Chi_Minh:

Transition at 1906-06-30T16:53:20Z from 25600 to 25590
Transition at 1911-04-30T16:53:30Z from 25590 to 25200
Transition at 1942-12-31T16:00:00Z from 25200 to 28800
Transition at 1945-03-14T15:00:00Z from 28800 to 32400
Transition at 1945-09-01T15:00:00Z from 32400 to 25200
Transition at 1947-03-31T17:00:00Z from 25200 to 28800
Transition at 1955-06-30T16:00:00Z from 28800 to 25200
Transition at 1959-12-31T16:00:00Z from 25200 to 28800
Transition at 1975-06-12T16:00:00Z from 28800 to 25200
1 Answers

One solution I worked out is, start with passing distantPast to nextDaylightSavingTimeTransition:

someTimeZone.nextDaylightSavingTimeTransition(after: .distantPast)

This gets you the first transition date. Then do:

// second transition date
someTimeZone.nextDaylightSavingTimeTransition(after:
    someTimeZone.nextDaylightSavingTimeTransition(after: .distantPast)
)

// third transition date
someTimeZone.nextDaylightSavingTimeTransition(after:
    someTimeZone.nextDaylightSavingTimeTransition(after:
        someTimeZone.nextDaylightSavingTimeTransition(after: .distantPast)
    )
)

and so on. Of course, we will put this in a loop. The offset before and after the transition can be found by secondsFromGMT(for:). We will pass a two dates that only differ by something like 1 second.

Also note that although this is named nextDaylightSavingTimeTransition, it also gives you non-DST transitions, which is exactly what you want, so that's great!

import Foundation

struct OffsetTransition {
    let instant: Date
    let offsetBefore: Int
    let offsetAfter: Int
}

var date = Date.distantPast
let timeZone = TimeZone(identifier: "Some Time Zone ID")!
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime]

var transitions = [OffsetTransition]()

while date < Date() {
    guard let instant = timeZone.nextDaylightSavingTimeTransition(after: date) else {
        break
    }
    let offsetBefore = timeZone.secondsFromGMT(for: instant.addingTimeInterval(-1))
    let offsetAfter = timeZone.secondsFromGMT(for: instant)
    if offsetBefore == offsetAfter { continue }
    
    transitions.append(.init(instant: instant, offsetBefore: offsetBefore, offsetAfter: offsetAfter))
    
    print("Transition at \(dateFormatter.string(from: instant)) from \(offsetBefore) to \(offsetAfter)")
    date = instant
}

Note the if offsetBefore == offsetAfter { continue } check. I added this check because without it, it will sometimes produce transitions with offsetBefore == offsetAfter. This could be an indication that I am doing something wrong, but this check seems to fix the problem...

The output of this almost matches the output of a similar code using Java's getTransitions. However, there is one other slight problem that I found with this solution. For some timezones, such as Europe/London, the transition from the local mean time to the standardised offset is missing, but for other timezones (e.g. Asia/Hong_Kong), it exists. For example, a similar code using getTransitions in Java would produce a "Transition at 1847-12-01T00:01:15Z from -75 to 0" for Europe/London, but the Swift code's output does not include this line. This is not too big of a problem for me though.

Related