轨道
/
Julia
Julia
/
练习
/
预约美容
预约美容

预约美容

学习练习

简介

日期和时间是我们教小孩子的东西。能有多难呢?

很多程序员都犯过这个错误,而随后的经历往往会对他们的健康和幸福造成不良影响。

任何用日期和时间做正经编程的人,至少都应该做好准备,去理解并缓解潜在的问题。

本文会尝试概述 Julia 如何处理日期和时间,但更多细节还需要查阅手册。 另外还有一本 Wikibook,有些读者可能会觉得它的讲解更清楚,其中还包含一张很有帮助的类型层级图。

Dates 模块

大部分基本功能都在Dates里。这个模块随默认安装一起提供,但并不属于标准库。

使用这个模块有两种方式:

  1. 加上import Dates,然后给所有函数调用都加上Dates.前缀
  2. 加上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())。
Note

为了减少视觉干扰,本文后续大部分示例代码都假设已经using Dates,调用函数时不再写出模块名Dates.。

日期与时间格式

日期和时间的写法有很多种,而且往往因文化而异。 “7/6/23”这种全数字日期有歧义、令人困惑,还让跨国公司付出过很多昂贵的代价(作者对此有惨痛的亲身经历)。

国际标准由ISO 8601定义,它有两个主要优点:

  • 解析又快又没有歧义。
  • 排序很简单,因为 datetime 可以当作普通文本按字母顺序排序。

在前面的例子里我们看到,Date.now()返回了2025-06-21T16:03:04.161。

它由几个部分组成:

  • YYYY-MM-DD表示日期部分。
  • hh:mm:ss表示时间部分。
  • 小数点后面可选地跟毫秒(或更小的单位)。

其他字符串格式会在后面的小节里讨论。

Dates

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

Times

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

DateTimes

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。

Periods

日期和时间的每个组成部分都有自己的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)。

1. 解析全数字格式的预约日期

实现 schedule_appointment() 函数,把以文本表示的预约日期解析成对应的 DateTime 格式。

注意,输入采用美式的月/日/年顺序。

julia> schedule_appointment("07/25/2023 13:45:00")
2023-07-25T13:45:00

2. 检查预约是否已经过去

实现 has_passed() 函数,它接受一个预约日期,并检查该预约是否在过去:

julia> has_passed(DateTime(1999, 12, 31, 9, 0, 0))
true

3. 检查预约是否在下午

实现 is_afternoon_appointment() 函数,它接受一个预约日期,并检查该预约是否在下午(>= 12:00 且 < 18:00):

julia> is_afternoon_appointment(DateTime(2023, 3, 29, 15, 0, 0))
true

4. 描述预约的时间和日期

实现 description() 函数,它接受一个预约日期,并返回对该日期和时间的描述:

julia> describe(DateTime(2023, 3, 29, 15, 0, 0))
"You have an appointment on Wednesday, March 29, 2023 at 15:00"

5. 返回周年纪念日

实现 anniversary_date() 函数,它返回今年的周年纪念日,也就是 9 月 15 日:

julia> anniversary_date()  # run during 2025
2025-09-15
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Julia Exercism

准备好开始 预约美容 了吗?

注册 Exercism,借助 35 个概念128 个练习 和真人导师指导,学习并掌握 Julia,全部免费。