Swift DateFormatter 格式测试
输入像 yyyy-MM-dd'T'HH:mm:ssZZZZZ 这样的 dateFormat 格式串,边输入边看到 DateFormatter 在你选的日期、Locale 和时区下到底输出什么。本页会标出真正引发 bug 的写法(该用 yyyy 却写了 YYYY、把 dd 写成 DD、hh 没有配 a、没有设 en_US_POSIX),列出每个符号并给出实时示例,还提供 Swift 代码,合适时附上 ISO8601DateFormatter 或 Date.FormatStyle 的写法。输出已在 Mac 上与 DateFormatter 逐一核对。所有内容都不离开你的浏览器。
字母是字段(yyyy、MM、HH)。其他文字放在单引号里:'T'、'at'。两个单引号('')输出一个单引号。
常用格式
en_US_POSIX 固定使用英文名称、公历,并严格按写下的格式输出,不受用户在“设置”里的选择影响。凡是给程序读的内容(接口、文件名、日志)都用它。
DateFormatter 输出
格式检查
这个格式没有已知的陷阱。
Swift 代码
DateFormatter
创建 DateFormatter 很慢:建一个,保存起来(例如放在 static let 里)反复使用。
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "Asia/Shanghai")!
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
let text = formatter.string(from: Date())
let parsed = formatter.date(from: text) // nil when the text does not matchISO8601DateFormatter 与 Date.ISO8601FormatStyle
专为这种格式设计,不需要 Locale。注意:带小数秒时,Date.ISO8601FormatStyle 可能比 DateFormatter 少一毫秒(.123 输出 .122),因为它截断二进制数值而不是四舍五入。
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime]
iso.timeZone = TimeZone(identifier: "Asia/Shanghai")!
let text = iso.string(from: Date())
// Or, without a formatter object (iOS 15, macOS 12 and later):
let style = Date.ISO8601FormatStyle(timeZoneSeparator: .colon, timeZone: TimeZone(identifier: "Asia/Shanghai")!)
let text2 = Date().formatted(style)这里每种代码写法都用 swiftc 编译运行过,输出与本页一致。
格式符号
DateFormatter 认识的每个字段,以及它在上面所选日期、Locale 和时区下的输出。点一下格式即可加到你的格式串里。
纪元
纪元:缩写、完整、窄格式。
年
日历年份。yy 是后两位,yyyy 补足四位。用这个。
按周计算的年份,配合 w 使用。在年末和年初几天与 y 不同。
扩展年份:类似 y,但 yy 不会截成两位。
U(干支纪年)和 r(对应的公历年)在公历里都输出普通年份。
季度
季度:数字、补零、缩写、完整、窄格式。
单独使用的季度(独立形式)。
月
月份:3、03,然后是名称。日期里用 MMM 和 MMMM。
单独使用的月份,用于标题。有些语言在日期里会变格,L 给出原形。
周
一年中的第几周。一周从哪天开始、第一周至少几天,取决于 Locale。
一个月中的第几周(某些 Locale 下,月初不满的一周记为 0)。
日
月中的日期。ddd 是 Apple 的扩展:英语里输出序数(7th)。
一年中的第几天,1 到 366。不是月中的日期。
本月第几个这样的星期几(2 = 第二个星期六)。
本地日期的儒略日数。
星期
星期名称:缩写、完整、窄格式、短格式。
从 Locale 的一周第一天算起的星期数字(美国星期日 = 1,德国星期一 = 1),eee 起是名称。
单独使用的星期(独立形式);c 和 cc 是本地数字。
上午、下午与时段
上午或下午:缩写、完整、窄格式。
类似 a,但在语言有对应词时,12:00 会输出“中午”之类的词。
灵活时段:凌晨、早上、下午、晚上。
小时
小时 1-12,需要配 a。
小时 0-23,固定格式通常用它。
小时 0-11,需要配 a。
小时 1-24(午夜是 24)。
分钟
分钟。
秒
秒。
秒的小数部分,按 S 的个数截断。只有毫秒是真实的。
从本地午夜起的毫秒数。
时区
区分夏令时的时区名:EST 和 EDT。Locale 没有短名称时退回为 GMT-5。
Z:-0500;ZZZZ:GMT-05:00;ZZZZZ:-05:00,UTC 时为 Z。
本地化的 GMT 偏移,短格式和长格式。
不区分夏令时的时区名:ET、Eastern Time。
时区标识:短代码、IANA 标识、城市、地区名称。
UTC 时输出 Z 的 ISO 8601 偏移。
从不输出 Z 的 ISO 8601 偏移。
名称(月份、星期、时段、时区名)取自 macOS 26 上 DateFormatter 的实际输出。较早的 iOS 和 macOS 版本使用较早的地区数据,时区名(z、zzzz、v、vvvv、VVVV)和 B 时段在那里可能不同。
它是怎么工作的
- •格式串按 Apple 的 ICU 库的规则拆分:同一字母连写是一个字段,单引号里的是普通文字,'' 是一个单引号,其他字符原样输出。
- •每个字段用取自 macOS 26 上 DateFormatter 实际输出的数据来打印:各 Locale 的月份、星期、季度、纪元名称,上午/下午和时段,以及时区名。没有使用浏览器自带的名称,因为 Apple 的名称有些不同(例如“中国大陆时间”,以及用“GMT+0”而不是“GMT”)。
- •所选时刻的 UTC 偏移来自浏览器的时区规则。周数遵循 DateFormatter 报告的各 Locale 规则:en_US、ja_JP 和 zh_Hans_CN 的一周从星期日开始,de_DE 从星期一开始,第一周至少需要几天也各不相同。
- •发布前,我们在 Mac 上把 130,800 种组合与 DateFormatter 逐一比对:所有符号和预设,在五个 Locale、三十个日期(YYYY 相关的新年前后、闰日、纽约和柏林的夏令时切换)和四个时区下输出,另外把时区符号放到全部十个时区里核对,结果全部一致。Swift 代码片段也在同一轮检查里编译运行过。
- •格式检查会列出真正引发 bug 的陷阱:按周计算的年份、一年中的第几天、没有上午/下午的 12 小时制、加引号的 'Z'、不带冒号的时区格式,以及 DateFormatter 拒绝或会崩溃的格式。
它做不到的事
- •它不是 Apple 的 ICU,而是独立的实现,在测试过的所有情况下都与 macOS 26 上的 DateFormatter 一致。较早的 iOS 和 macOS 版本带的是较早的地区数据,在那些系统上,时区名(z、zzzz、v、vvvv、VVVV)和 B 时段只是近似结果。
- •只支持五个 Locale 和十个时区,日期范围是 2000 年到 2100 年。其他时区的名称需要先与 Apple 核对,所以暂不提供。
- •它不模拟设备上的设置。除 en_US_POSIX 外的任何 Locale,用户的 24 小时制开关、日历类型(佛历、和历)或一周的第一天都可能改变真机上 DateFormatter 的输出。
- •它只做格式化,不做解析。date(from:) 有自己的规则(宽松解析、两位数年份、默认字段),本页不模拟。
- •时间精确到毫秒。DateFormatter 同样只到毫秒,所以 SSSS 及更长的格式后面都是 0。
- •对会让 DateFormatter 崩溃的格式(六个或更多 B,或中午时六个或更多 b),本页给出提示,而不是模拟崩溃。
常见问题
- 为什么 YYYY 显示的年份不对?
- YYYY 是“周所属的年份”,不是日历年份,只有和 w(一年中的第几周)一起用才有意义。新年前后的几天,一个日期可能属于下一年的第 1 周或上一年的最后一周:在 en_US 下,2024 年 12 月 31 日用 YYYY 会输出 2025。普通日期请用 yyyy;这个 bug 每年只出现几天,所以常常被带到线上。
- 为什么要把 Locale 设为 en_US_POSIX?
- 因为固定格式不能依赖用户。使用设备的 Locale 时,DateFormatter 可能换成佛历或和历(年份变成 2569 而不是 2026)、把名称本地化,或按用户的 24 小时制设置改写小时,解析随之失败。en_US_POSIX 保证不变。Apple 的技术问答 QA1480 建议所有固定格式的日期都这样做,同时也要明确设置格式化器的时区。
- 怎么解析带小数秒的 ISO 8601 日期?
- 用 ISO8601DateFormatter,formatOptions 设为 [.withInternetDateTime, .withFractionalSeconds]。这样设置后它会拒绝不带小数的字符串,所以如果接口两种都会发,就准备一个带小数、一个不带小数的格式化器依次尝试。用 DateFormatter 的话,格式是 yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX,配 en_US_POSIX。JSONDecoder 的 .iso8601 策略完全不接受小数秒,请用 .custom 配合格式化器。
- 用 ISO8601DateFormatter 还是 DateFormatter?
- ISO 8601 文本用 ISO8601DateFormatter:没有会被忘记设置的 Locale,UTC 时输出 Z,其他时区输出带冒号的偏移,选项直接说明要哪些部分(.withFullDate、.withInternetDateTime)。其他固定格式,例如 HTTP 日期(EEE, dd MMM yyyy HH:mm:ss 'GMT'),以及用 dateStyle/timeStyle 显示的文字,用 DateFormatter。两者创建都很慢,建一个实例反复使用。
- 应该改用 Date.FormatStyle 吗?
- 在 iOS 15、macOS 12 及以上,通常可以。给人看的文字,date.formatted(date: .abbreviated, time: .shortened) 不需要格式串,就会遵循用户的 Locale 和设置。固定格式可以用 Date.ISO8601FormatStyle 和 Date.VerbatimFormatStyle,用有类型的字段代替格式串;只要格式串里每个字段都有完全对应的写法,本页就会给出 verbatim 版本。这些样式值创建起来很便宜。
- 怎样尊重用户的 12/24 小时制设置?
- 显示用的时间不要写死 hh:mm a 或 HH:mm。用 timeStyle = .short、Date.FormatStyle,或 setLocalizedDateFormatFromTemplate("jmm"):j 表示按用户选择的小时制显示小时。给数据用的固定格式继续配 en_US_POSIX,那里不应受用户设置影响。