如何找到一个时区的所有历史偏移转换日期?

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

我正在尝试编写一个显示有关给定时区信息的应用程序。它显示该时区的哪些时间段正在观察 DST,哪些时间段不是。最重要的是,它突出显示了时区 不规则变化 的时间,例如英国在二战期间观察 夏令时,或者萨摩亚在 2011 年跳过了一天.

为此,我需要获取存储在 TZ 数据库中的所有历史时区偏移转换的列表(我认为每个 macOS/iOS 设备中都有数据库的副本)。更具体地说,“过渡”(类似于 java.time.zone.ZoneOffsetTransition)由以下 3 件事建模:

据我从 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:) 中看到的,但这只告诉我一个转换的转换日期,当给定一个日期时,我也不确定要给出什么日期。

如何获取转换列表?

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

我想出的一个解决方案是,首先将 distantPast 传递给 nextDaylightSavingTimeTransition:

someTimeZone.nextDaylightSavingTimeTransition(after: .distantPast)

这为您提供了第一个过渡日期。然后做:

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

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

等等。当然,我们会把它放在一个循环中。通过secondsFromGMT(for:)可以找到过渡前后的偏移量。我们将传递两个仅相差 1 秒的日期。

另请注意,虽然它被命名为 nextDaylightSavingTimeTransition,但它也为您提供非 DST 转换,这正是您想要的,太棒了!

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
}

注意 if offsetBefore == offsetAfter { continue } 检查。我添加了这个检查,因为没有它,它有时会产生 offsetBefore == offsetAfter 的转换。这可能表明我做错了什么,但这项检查似乎解决了问题...

此代码的输出几乎与使用 Java 的 getTransitions 的类似代码的输出相匹配。但是,我发现这个解决方案还有一个小问题。对于某些时区,例如 Europe/London,从 local mean time 到标准化偏移量的过渡缺失,但对于其他时区(例如 Asia/Hong_Kong),它存在。例如,在 Java 中使用 getTransitions 的类似代码将为 Europe/London 生成“在 1847-12-01T00:01:15Z 从 -75 到 0 的转换”,但是 Swift 代码的输出不包括这一行。不过这对我来说不是什么大问题。