如何从 NSTimeZone 获取 GNU Lib C TZ 格式输出?
Posted
技术标签:
【中文标题】如何从 NSTimeZone 获取 GNU Lib C TZ 格式输出?【英文标题】:How can I get GNU Lib C TZ format output from NSTimeZone? 【发布时间】:2021-09-23 16:55:28 【问题描述】:我需要将一个远程时钟的时区信息设置为ios设备上的那个。
远程时钟仅支持GNU lib C TZ format的:
std offset dst [offset],start[/time],end[/time]
例如:EST+5EDT,M3.2.0/2,M11.1.0/2
所以我需要从 Swift 中的 NSTimeZone.local 时区生成类似于上面的字符串。似乎无法访问当前时区规则,因为它们会在 IANA TZ database 中生成输出。
如果没有在应用程序中缓存 TZ 数据库的本地副本的可怕想法,是否可以做到这一点?
更新:
即使通过其他编程语言,我也找不到任何有用的东西。我能找到的最好的方法基本上是在 linux 中解析 tzfile 并制作我自己的包含信息的 NSDictionary。
【问题讨论】:
明确地说,您是否希望从本地TimeZone
实例中生成 std offset dst [offset],start[/time],end[/time]
形式的字符串,或解析 std offset dst [offset],start[/time],end[/time]
到本地 TimeZone
实例? (似乎是第一个,但不是 100% 清楚)
@ItaiFerber 更新了问题以反映。我正在寻找从本地 ios 时区生成字符串。
我现在正在整理一个答案,但老实说,从一个工作的 TimeZone
对象到这样的字符串是相当普遍的假设,而且做起来相对昂贵,主要是因为可以返回相关信息的 API 比设备上 IANA TZ DB 中的信息更通用。你能在这里提供更多关于远程端的信息吗?它连接到互联网吗?您是向远程设备发送许多这样的字符串,还是只发送一个与 iOS 设备上当前 TZ 对应的字符串?
另外,您是否可以控制远程设备和发送过来的数据,或者这是一成不变的?
@ItaiFerber 感谢您的帮助。我们需要做的就是获取 iOS 本地时区的单个 tzstr 兼容字符串。这需要在 Swift 端完成,远程时钟只能解析单个 TZ 格式的字符串以产生正确的时间。到目前为止,我发现 C# 能够提供 TZ 规则并为其编写一个 stringify 实用程序,而忽略过期和未来的规则。
【参考方案1】:
这是一次有趣的探索,主要是因为将数据调整为正确的格式非常复杂。问题组件:
我们需要适用于给定时区的“当前”TZ 数据库规则。这有点牵强的概念,因为:
对于大多数应用程序,Darwin 平台实际上并不直接使用 TZ 数据库,而是使用 ICU 的时区数据库,它有不同的格式并且更复杂。即使您生成这种格式的字符串,它也不一定描述设备上的实际时间行为
虽然可以在 iOS 上动态读取和解析 TZ 数据库,但不能保证 TZ 数据库本身以此处所需的格式存储信息。 rfc8536,管理时区信息格式的 RFC 对您想要的格式进行了如下说明:
版本 3 TZif 文件中的 TZ 字符串可以对 POSIX TZ 字符串使用以下扩展名。这些扩展使用 [POSIX] 的“基本定义”卷第 8.3 节的术语进行描述。
示例:3,M3.5.0/-2,M10.5.0/-1 示例:EST5EDT,0/0,J365/25
在深入研究 iOS TZ 数据库时,我发现一些数据库条目确实以这种格式在文件末尾提供了规则,但它们似乎是少数。您可以动态解析这些,但可能不值得
所以,我们需要使用 API 来生成这种格式的字符串。
为了生成在给定日期至少大致正确的“规则”,您需要了解有关该日期前后 DST 转换的信息。这是一个非常棘手的话题,因为 DST 规则一直在变化,而且并不总是像你希望的那样有意义。至少:
北半球的许多时区从春季开始到秋季结束 DST 南半球的许多时区从秋季开始到春季结束 DST 某些时区不遵守 DST(全年采用标准时间) 某些时区不遵守 DST,并且全年处于日光时间由于规则非常复杂,因此本答案的其余部分假定您可以生成代表特定时间日期的“足够好”的答案,并且愿意在某个时间向您的时钟发送更多字符串将来需要更正时。例如,为了描述“现在”,我们将假设根据最后一个 DST 转换(如果有)和下一个 DST 转换(如果有)生成规则“足够好”,但是这可能不起作用 适用于多个时区的所有情况
Foundation 以TimeZone.nextDaylightSavingTimeTransition
/TimeZone.nextDaylightSavingTimeTransition(after:)
的形式提供TimeZone
的夏令时转换信息。然而,令人沮丧的是,没有办法获得有关以前 DST 转换的信息,因此我们需要纠正这一点:
Foundation 的本地化支持(包括日历和时区)直接基于 the ICU library,它在所有 Apple 平台上内部提供。 ICU确实提供了一种获取有关先前 DST 转换信息的方法,但 Foundation 只是不将其作为 API 提供,因此我们需要自己公开它
ICU 是 Apple 平台上的半私有库。该库保证存在,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_getTimeZoneTransitionDate
的值为UCAL_TZ_TRANSITION_NEXT
,所以我们可以通过使用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"
,乍一看对我来说似乎是正确的,但许多其他边缘情况可能值得探索:
TRANSITION_PREVIOUS
与 TRANSITION_PREVIOUS_INCLUSIVE
)
始终为标准/夏令时的时区
非标准日光/时区偏移
这还有很多,一般来说,我建议尝试找到一种在此设备上设置时间的替代方法(最好使用 named 时区),但这可能有希望至少让你开始。
【讨论】:
感谢您的详细回答。所以您建议创建一个对“年份”有效的 TZ 字符串,并将“WART4WARST,J1/0,J365/25”样式转换为 M1.1.0。需要每年更新一次时钟上的 TZ str 以保持正确。 RE: TRANSITION_PREVIOUS/ICU lib: 不能用过去的日期(例如今年 1 月 1 日)连续调用 nextDaylightSavingTimeTransition(after:) 并获取上一个和下一个转换吗?总的来说,我会说这种方法比解析 TZfile 更好,因为它依赖 iOS 来保持最新的 TZdb 并且需要每年更新 TZ 作为折衷方案。 @MandoMando 是的,这将在“J1/0”上产生“M1.1.0”,但您可以轻松调整(您可以检查开始日期是否为== year.start
并写出“J1/0”,虽然调整开始时间有点烦人[想想 1/1 的 DST,比如说,凌晨 4:00])。
@MandoMando 回复:TRANSITION_PREVIOUS
——这是个好问题。最初,我沿着 ICU 路线走,因为我考虑过在最后一次 DST 转换 > 1 年前表达行为并将其编码到规则中,但在意识到规则只需要适用于这个日历年之后没有适应。因此,您可以通过在year.start - 1
之后搜索“下一个转换”来大大简化。要记住的一件事是,有些年份可能有多个转换,因此您可能需要向前搜索直到transitionAfter(previous) == transitionAfter(inputDate)
@MandoMando 一般来说,你必须小心,因为 TZ 规则在很大程度上是政治性的。 (如果他们切换到/从 DST 或完全切换 TZ 定义,有些年份看起来会很奇怪。)我主要建议缓存最新生成的字符串并在每次启动时重新生成一次(或类似的)——如果你检测到差异,更新时钟。
顺便说一句,我向 Foundation 提交了反馈,以便在本机提供 TimeZone.previousDaylightSavingTimeTransition
(FB9659430)。以上是关于如何从 NSTimeZone 获取 GNU Lib C TZ 格式输出?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 NSTimeZone -timeZoneWithName: 与 Rails ActiveSupport 中的城市名称?
OSError:/lib/aarch64-linux-gnu/libgomp.so.1:无法在静态 TLS 块中分配内存