日期和时间是我们教小孩子的东西。能有多难呢?
很多程序员都犯过这个错误,而随后的经历往往会对他们的健康和幸福造成不良影响。
任何用日期和时间做正经编程的人,至少都应该做好准备,去理解并缓解潜在的问题。
本文会尝试概述 Julia 如何处理日期和时间,但更多细节还需要查阅手册。 另外还有一本 Wikibook,有些读者可能会觉得它的讲解更清楚,其中还包含一张很有帮助的类型层级图。
Dates 模块大部分基本功能都在Dates里。这个模块随默认安装一起提供,但并不属于标准库。
使用这个模块有两种方式:
import Dates,然后给所有函数调用都加上Dates.前缀using Dates,这会把整个模块引入当前命名空间(少数有问题的名字除外)。# option 1
julia> import Dates
julia> Dates.now()
2025-06-21T16:02:39.080
julia> now() # wrong: need to include `Dates.` after an import
ERROR: UndefVarError: `now` not defined in `Main`
# option 2
julia> using Dates
julia> now() # functions are now in the namespace
2025-06-21T16:03:04.161
Dates模块又大又复杂,但它提供四大类功能:
Dates.Date,包含日、月、年,但不含时刻。Dates.Time,包含时刻,但不含日期。Dates.DateTime,把日期和时间组合在一起。Dates.Period,用来处理时间或日期的间隔。请注意,DateTime通常_不_包含时区信息(Python 会把这种 datetime 叫作“naive”datetime)。
有一个例外是now(),它从系统时钟获取本地时间,必要时还可以把它转换成 UTC。
最常见需求大概是:
Dates类型。Dates类型中取出所需的数值或字符串格式。Date、Time或DateTime值加上一个偏移量。today())和/或时间(用now())。为了减少视觉干扰,本文后续大部分示例代码都假设已经using Dates,调用函数时不再写出模块名Dates.。
日期和时间的写法有很多种,而且往往因文化而异。 “7/6/23”这种全数字日期有歧义、令人困惑,还让跨国公司付出过很多昂贵的代价(作者对此有惨痛的亲身经历)。
国际标准由ISO 8601定义,它有两个主要优点:
在前面的例子里我们看到,Date.now()返回了2025-06-21T16:03:04.161。
它由几个部分组成:
YYYY-MM-DD表示日期部分。hh:mm:ss表示时间部分。其他字符串格式会在后面的小节里讨论。
Dates.Date是一个相对简单的仅日期类型,不理解时间,也不理解时区。
# constructor with year, month, day arguments
julia> date1 = Date(2025, 06, 23)
2025-06-23
# constructor with ISO 8601 string
julia> date2 = Date("2025-06-23")
2025-06-23
julia> date1 == date2
true
julia> typeof(date1)
Date
日期的各个组成部分都有对应的访问函数,这些函数成对出现,靠大小写区分。
# lowercase returns an integer
julia> month(date1)
6
julia> typeof(month(date1))
Int64
# Propercase returns a Period type
julia> Month(date1)
6 months
julia> typeof(Month(date1))
Month
有一系列函数对日历有一定了解。 下面这个例子只展示了其中几个。
julia> date1
2025-06-23
julia> dayofweek(date1) # 1 = Monday, 7 = Sunday
1
julia> dayname(date1)
"Monday"
julia> daysinmonth(date1)
30
julia> isleapyear(date1)
false
julia> lastdayofmonth(date1)
2025-06-30
Dates.Time是基本的仅时间类型。
它不理解日期:到了午夜,时间会自动回到Time(0, 0, 0)。
完整的构造函数格式是Time(hour, min, sec, millisec, microsec),或者等价的 ISO 8601 写法。
所有参数都是可选的,默认值为0。
julia> Time(14, 57, 25, 142)
14:57:25.142
julia> Time("14:57:25.142")
14:57:25.142
julia> Time(14, 57, 25, 142, 32)
14:57:25.142032
和Date一样,各个组成部分可以通过访问函数取得:
julia> minute(t) # => integer
57
julia> Minute(t) # => Period
57 minutes
Dates.DateTime结合了Date和Time类型的大部分特性。
julia> dt1 = DateTime(2025, 6, 23, 14, 57, 25, 142)
2025-06-23T14:57:25.142
# date1 and t were defined in previous examples
julia> dt2 = DateTime(date1, t)
2025-06-23T14:57:25.142
年、月、日参数默认为1,时间参数全部默认为0。
要把这些参数的位置都记清楚并不容易,所以输入时用 ISO 格式可能更好。
也可以像上面的例子那样,用一个Date和一个Time构造出DateTime。
日期和时间的每个组成部分都有自己的Period类型,从Year一直到Nanosecond,表示一段时间间隔。
它们在做运算时特别有用。
julia> dt1
2025-06-23T14:57:25.142
julia> Year(21)
21 years
julia> Second(61)
61 seconds
julia> dt1 + Year(21) - Second(61)
2046-06-23T14:56:24.142
julia> Date(2025, 07, 03) - Date(2025, 04, 25)
69 days
处理Period类型时,Julia 会尽量聪明地应对月长之类的日历不规则情况,必要时进行取整。
# February has fewer days than January
julia> Date(2025, 1, 31) + Month(1)
2025-02-28
前面提到过 ISO 8601 标准,它是 Julia 在格式化或解析字符串时默认采用的日期和时间表示法。
显然,还有很多其他格式被广泛使用,而且往往带着文化特色。
为了应对这些格式,Julia 提供了DateFormat类型,可以用任意复杂的格式字符串来构造。
# dateformat"" string syntax
julia> dtfmt = dateformat"d/m/y"
dateformat"d/m/y"
julia> typeof(dtfmt)
DateFormat{Symbol("d/m/y"), Tuple{Dates.DatePart{'d'}, Dates.Delim{Char, 1}, Dates.DatePart{'m'}, Dates.Delim{Char, 1}, Dates.DatePart{'y'}}}
# DateFormat constructor
julia> DateFormat("d/m/y")
dateformat"d/m/y"
上面的例子非常简单,只是欧洲常见的日/月/年格式的日期。 这里展示了语法的两种写法,但结果完全相同。
之后就可以用DateFormat来解析任何合适的字符串:
julia> DateTime("14/7/2023", dtfmt)
2023-07-14T00:00:00
创建DateFormat的开销不小,所以像这里用dtfmt那样只创建一次,在循环处理大量日期和时间字符串时对性能很有帮助。
格式可以比这复杂得多,详细内容请参阅手册。
同一个DateFormat也可以用来把日期和时间输出成任意想要的字符串格式,用的是 Dates.format() 函数。
遗憾的是,format似乎存在命名空间冲突(至少 Julia 1.11.5 是这样),所以即使已经using Dates,最好还是加上Dates.前缀。
julia> dt1
2025-06-23T14:57:25.142
julia> Dates.format(dt1, dateformat"e, u d")
"Mon, Jun 23"
如文档所述,"e, u d"会给出星期几和月份的缩写文本表示。
文本表示默认使用英语。 也可以添加其他语言,只是目前这方面的支持还不太理想。
在这个练习中,你将为一个位于纽约的美容院编写预约安排程序,这家美容院于 2012 年 9 月 15 日开业。
一共有五个任务,全都与预约日期有关。 时间采用 24 小时制(没有 AM/PM)。
实现 schedule_appointment() 函数,把以文本表示的预约日期解析成对应的 DateTime 格式。
注意,输入采用美式的月/日/年顺序。
julia> schedule_appointment("07/25/2023 13:45:00")
2023-07-25T13:45:00
实现 has_passed() 函数,它接受一个预约日期,并检查该预约是否在过去:
julia> has_passed(DateTime(1999, 12, 31, 9, 0, 0))
true
实现 is_afternoon_appointment() 函数,它接受一个预约日期,并检查该预约是否在下午(>= 12:00 且 < 18:00):
julia> is_afternoon_appointment(DateTime(2023, 3, 29, 15, 0, 0))
true
实现 description() 函数,它接受一个预约日期,并返回对该日期和时间的描述:
julia> describe(DateTime(2023, 3, 29, 15, 0, 0))
"You have an appointment on Wednesday, March 29, 2023 at 15:00"
实现 anniversary_date() 函数,它返回今年的周年纪念日,也就是 9 月 15 日:
julia> anniversary_date() # run during 2025
2025-09-15