我需要将远程时钟的时区信息设置为iOS设备上的时区信息。
远程时钟仅支持 GNU lib C TZ 格式:std offset dst [offset],start[/time],end[/time]
例如:EST+5EDT,M3.2.0/2,M11.1.0/2
所以我需要在 Swift 中从NSTimeZone.local时区生成一个类似于上面的字符串。 似乎无法访问当前时区规则,因为它们将在 IANA TZ 数据库中生成输出。
如果没有在应用程序中缓存TZ数据库的本地副本的可怕想法,可以做到这一点吗?
更新:
即使通过其他编程语言,我也找不到任何有用的东西。 我能找到的最好的基本上是在linux中解析tzfile,并制作我自己的NSDictionary包含信息。
这是一个有趣的探索,主要是因为将数据调整成正确的格式非常复杂。问题组件:
-
我们需要适用于给定时区的"当前"TZ 数据库规则。这是一个有点加载的概念,因为:
-
Darwin平台实际上并不直接将TZ数据库用于大多数应用程序,而是使用ICU的时区数据库,该数据库具有不同的格式且更复杂。即使生成此格式的字符串,也不一定描述设备上的实际时间行为
-
虽然可以在 iOS 上动态读取和解析 TZ 数据库,但 TZ 数据库本身不能保证以此处所需的格式存储信息。 rfc8536,管理时区信息格式的 RFC 对您想要的格式说明如下:
版本 3 TZif 文件中的 TZ 字符串可以使用以下扩展名来扩展 POSIX TZ 字符串。 这些扩展使用 [POSIX] 的"基本定义"卷第 8.3 节的术语进行描述。
示例:<-03>3<-02>,M3.5.0/-2,M10.5.0/-1
示例:EST5EDT,0/0,J365/25在浏览iOS TZ数据库时,我发现一些数据库条目确实在文件末尾以这种格式提供了规则,但它们似乎是少数。您可以动态解析这些内容,但这可能不值得
因此,我们需要使用 API 来生成这种格式的字符串。
-
-
为了生成在给定日期至少大致正确的"规则",您需要了解有关该日期前后 DST 转换的信息。这是一个非常棘手的话题,因为 DST 规则一直在变化,并不总是像您希望的那样有意义。至少:
- 北半球的许多时区都遵守夏令时,从春季开始,到秋季结束
- 南半球的许多时区都遵守夏令时,从秋季开始,到春季结束
- 某些时区不遵守 DST(全年采用标准时间)
- 某些时区不遵守夏令时,全年处于夏令时
由于规则非常复杂,因此此答案的其余部分假设您可以生成一个"足够好"的答案,该答案代表特定的时间,并且愿意在将来需要更正的某个时间向您的时钟发送更多字符串。 例如,为了描述"现在",我们将假设基于上一个 DST 转换(如果有)和下一个 DST 生成规则过渡(如果有的话)"足够好",但这可能不适用于许多时区的所有情况
-
基金会以
TimeZone.nextDaylightSavingTimeTransition
/TimeZone.nextDaylightSavingTimeTransition(after:)
的形式提供有关TimeZone
的DST转换信息。然而,令人沮丧的是,没有办法获取有关以前的DST转换的信息,因此我们需要纠正这一点:-
Foundation 的本地化支持(包括日历和时区)直接基于 ICU 资料库,该资料库在所有 Apple 平台上内部提供。ICU确实提供了一种获取有关以前 DST 转换的信息的方法,但 Foundation 只是不提供它作为 API,因此我们需要自己公开它。
-
ICU是苹果平台上的半私人图书馆。该库保证存在,Xcode 将为您提供
。libicucore.tbd
以在<Project> > <Target> > Build Phases > Link Binary with Libraries
中链接,但实际的标题和符号不会直接暴露给应用程序。您可以成功链接libicucore
,但您需要在导入到 Swift 中的 Obj-C 标头中转发声明我们需要的功能 -
在 Swift 项目中的某个地方,我们需要公开以下 ICU 功能:
#include <stdint.h> typedef void * _Nonnull UCalendar; typedef double UDate; typedef int8_t UBool; typedef uint16_t UChar; typedef enum UTimeZoneTransitionType { UCAL_TZ_TRANSITION_NEXT, UCAL_TZ_TRANSITION_NEXT_INCLUSIVE, UCAL_TZ_TRANSITION_PREVIOUS, UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE, } UTimeZoneTransitionType; typedef enum UCalendarType { UCAL_TRADITIONAL, UCAL_DEFAULT, UCAL_GREGORIAN, } UCalendarType; typedef enum UErrorCode { U_ZERO_ERROR = 0, } UErrorCode; UCalendar * _Nullable ucal_open(const UChar *zoneID, int32_t len, const char *locale, UCalendarType type, UErrorCode *status); void ucal_setMillis(const UCalendar * _Nonnull cal, UDate date, UErrorCode * _Nonnull status); UBool ucal_getTimeZoneTransitionDate(const UCalendar * _Nonnull cal, UTimeZoneTransitionType type, UDate * _Nonnull transition, UErrorCode * _Nonnull status);
这些都是前向声明/常量,因此无需担心实现(因为我们通过针对
libicucore
链接来获得它)。 -
您可以在
UTimeZoneTransitionType
中看到值 —TimeZone.nextDaylightSavingTimeTransition
只是调用值为UCAL_TZ_TRANSITION_NEXT
的ucal_getTimeZoneTransitionDate
,因此我们可以通过使用UCAL_TZ_TRANSITION_PREVIOUS
调用方法来提供大致相同的功能:extension TimeZone { func previousDaylightSavingTimeTransition(before: Date) -> Date? { // We _must_ pass a status variable for `ucal_open` to write into, but the actual initial // value doesn't matter. var status = U_ZERO_ERROR // `ucal_open` requires the time zone identifier be passed in as UTF-16 code points. // `String.utf16` doesn't offer a contiguous buffer for us to pass directly into `ucal_open` // so we have to create our own by copying the values into an `Array`, then let timeZoneIdentifier = Array(identifier.utf16) guard let calendar = Locale.current.identifier.withCString({ localeIdentifier in ucal_open(timeZoneIdentifier, // implicit conversion of Array to a pointer, but convenient! Int32(timeZoneIdentifier.count), localeIdentifier, UCAL_GREGORIAN, &status) }) else { // Figure out some error handling here -- we failed to find a "calendar" for this time // zone; i.e., there's no time zone date for this time zone. // // With more enum cases copied from `UErrorCode` you may find a good way to report an // error here if needed. `u_errorName` turns a `UErrorCode` into a string. return nil } // `UCalendar` functions operate on the calendar's current timestamp, so we have to apply // `date` to it. `UDate`s are the number of milliseconds which have passed since January 1, // 1970, while `Date` offers its time interval in seconds. ucal_setMillis(calendar, before.timeIntervalSince1970 * 1000.0, &status) var result: UDate = 0 guard ucal_getTimeZoneTransitionDate(calendar, UCAL_TZ_TRANSITION_PREVIOUS, &result, &status) != 0 else { // Figure out some error handling here -- same as above (check status). return nil } // Same transition but in reverse. return Date(timeIntervalSince1970: result / 1000.0) } }
-
因此,在所有这些到位的情况下,我们可以填写一个粗略的方法,以您需要的格式生成字符串:
extension TimeZone {
struct Transition {
let abbreviation: String
let offsetFromGMT: Int
let date: Date
let components: DateComponents
init(for timeZone: TimeZone, on date: Date, using referenceCalendar: Calendar) {
abbreviation = timeZone.abbreviation(for: date) ?? ""
offsetFromGMT = timeZone.secondsFromGMT(for: date)
self.date = date
components = referenceCalendar.dateComponents([.month, .weekOfMonth, .weekdayOrdinal, .hour, .minute, .second], from: date)
}
}
func approximateTZEntryRule(on date: Date = Date(), using calendar: Calendar? = nil) -> String? {
var referenceCalendar = calendar ?? Calendar(identifier: .gregorian)
referenceCalendar.timeZone = self
guard let year = referenceCalendar.dateInterval(of: .year, for: date) else {
return nil
}
// If no prior DST transition has ever occurred, we're likely in a time zone which is either
// standard or daylight year-round. We'll cap the definition here to the very start of the
// year.
let previousDSTTransition = Transition(for: self, on: previousDaylightSavingTimeTransition(before: date) ?? year.start, using: referenceCalendar)
// Same with the following DST transition -- if no following DST transition will ever come,
// we'll cap it to the end of the year.
let nextDSTTransition = Transition(for: self, on: nextDaylightSavingTimeTransition(after: date) ?? year.end, using: referenceCalendar)
let standardToDaylightTransition: Transition
let daylightToStandardTransition: Transition
if isDaylightSavingTime(for: date) {
standardToDaylightTransition = previousDSTTransition
daylightToStandardTransition = nextDSTTransition
} else {
standardToDaylightTransition = nextDSTTransition
daylightToStandardTransition = previousDSTTransition
}
let standardAbbreviation = daylightToStandardTransition.abbreviation
let standardOffset = formatOffset(daylightToStandardTransition.offsetFromGMT)
let daylightAbbreviation = standardToDaylightTransition.abbreviation
let startDate = formatDate(components: standardToDaylightTransition.components)
let endDate = formatDate(components: daylightToStandardTransition.components)
return "(standardAbbreviation)(standardOffset)(daylightAbbreviation),(startDate),(endDate)"
}
/* These formatting functions can be way better. You'll also want to actually cache the
DateComponentsFormatter somewhere.
*/
func formatOffset(_ dateComponents: DateComponents) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = .dropTrailing
return formatter.string(from: dateComponents) ?? ""
}
func formatOffset(_ seconds: Int) -> String {
return formatOffset(DateComponents(second: seconds))
}
func formatDate(components: DateComponents) -> String {
let month = components.month ?? 0
let week = components.weekOfMonth ?? 0
let day = components.weekdayOrdinal ?? 0
let offset = formatOffset(DateComponents(hour: components.hour, minute: components.minute, second: components.second))
return "M(month).(week).(day)/(offset)"
}
}
请注意,这里有很多需要改进的地方,尤其是在清晰度和性能方面。(格式化程序是出了名的昂贵,所以你肯定想要缓存它们。这目前也只生成扩展形式的日期"Mm.w.d"
而不是儒略日,但可以附加。该代码还假设将无限规则限制为当前日历年是"足够好的",因为这就是 GNU C 库文档似乎暗示的,例如始终处于标准/夏令时的时区。(这也不能识别像GMT/UTC这样的众所周知的时区,这可能足以写出"GMT"。
我没有在不同的时区广泛测试过这段代码,上面的代码应该被视为额外迭代的基础。对于我的America/New_York
时区,这会产生"EST-5EDT,M3.3.2/3,M11.2.1/1"
,乍一看对我来说似乎是正确的,但许多其他边缘情况可能值得探索:
- 年初/年末的边界条件
- 给出与 DST 转换完全匹配的日期(考虑
TRANSITION_PREVIOUS
与TRANSITION_PREVIOUS_INCLUSIVE
) - 始终为标准/日光的时区
- 非标准日光/时区偏移
这还有很多,一般来说,我建议尝试找到一种在此设备上设置时间的替代方法(最好使用命名时区),但这至少可以让你入门。