Wait the light to fall

Julia 中的日期和时间

焉知非鱼

Dates in Julia

Dates 模块的加载和使用

在 Julia 的 Pkg REPL 中, 输入 add Dates 添加 Dates 模块。回到 Julia 的 REPL 中, 输入:

julia> using Dates

julia> DateTime(2020)
2020-01-01T00:00:00

julia> typeof(DateTime(2020))
DateTime

julia> DateTime(2020,8,1)
2020-08-01T00:00:00

julia> DateTime(2020,8,1,12)
2020-08-01T12:00:00

julia> DateTime(2020,8,1,12,30)
2020-08-01T12:30:00

julia> DateTime(2020,8,1,12,30,59)
2020-08-01T12:30:59

julia> DateTime(2020,8,1,12,30,59, 999)
2020-08-01T12:30:59.999

julia> Date(2020, 8)
2020-08-01

julia> Date(2020, 8, 1)
2020-08-01

julia> Date(Dates.Year(2020),Dates.Month(8),Dates.Day(1))
2020-08-01

julia> Date(Dates.Month(8),Dates.Year(2020))
2020-08-01

Date 和 DateTime 的算术操作

julia> dt = Date(2012,2,29)
2012-02-29

julia> dt2 = Date(2000,2,1)
2000-02-01

julia> dump(dt)
Date
  instant: Dates.UTInstant{Day}
    periods: Day
      value: Int64 734562

julia> dump(dt2)
Date
  instant: Dates.UTInstant{Day}
    periods: Day
      value: Int64 730151

julia> dt > dt2
true

julia> dt != dt2
true

julia> dt + dt2
ERROR: MethodError: no method matching +(::Date, ::Date)
[...]

julia> dt * dt2
ERROR: MethodError: no method matching *(::Date, ::Date)
[...]

julia> dt / dt2
ERROR: MethodError: no method matching /(::Date, ::Date)

julia> dt - dt2
4411 days

julia> typeof(dt - dt2)
Day

julia> dt2 - dt
-4411 days

julia> dt = DateTime(2012,2,29)
2012-02-29T00:00:00

julia> dt2 = DateTime(2000,2,1)
2000-02-01T00:00:00

julia> dt - dt2
381110400000 milliseconds

julia> typeof(dt - dt2)
Millisecond

访问器函数

因为 DateDateTime 类型被存储为单个 Int64 值,所以日期部分或字段可以通过访问器函数进行检索。小写访问器函数以整数形式返回字段。

julia> t = Date(2014, 1, 31)
2014-01-31

julia> Dates.year(t)
2014

julia> Dates.month(t)
1

julia> Dates.week(t)
5

julia> Dates.day(t)
31

而专有形式返回相应 Period 类型中的相同值。

julia> Dates.Year(t)
2014 years

julia> Dates.Day(t)
31 days

Julia 还提供了复合方法,因为在同时需要多个字段的情况下,这些方法提供了一种效率衡量标准。

julia> Dates.yearmonth(t)
(2014, 1)

julia> Dates.monthday(t)
(1, 31)

julia> Dates.yearmonthday(t)
(2014, 1, 31)

也可以访问底层 UTInstant 或整数值。

julia> dump(t)
Date
  instant: Dates.UTInstant{Day}
    periods: Day
      value: Int64 735264

julia> t.instant
Dates.UTInstant{Day}(Day(735264))

julia> Dates.value(t)
735264

查询函数

查询函数提供关于 TimeType 的历法信息。它们包括关于一周中的某一天的信息。

julia> t = Date(2014, 1, 31)
2014-01-31

julia> Dates.dayofweek(t)
5

julia> Dates.dayname(t)
"Friday"

julia> Dates.dayofweekofmonth(t) # 5th Friday of January
5

julia> Dates.monthname(t)
"January"

julia> Dates.daysinmonth(t)
31

以及 TimeType 的年份和季度信息。

julia> Dates.isleapyear(t)
false

julia> Dates.dayofyear(t)
31

julia> Dates.quarterofyear(t)
1

julia> Dates.dayofquarter(t)
31

daynamemonthname 方法也可以使用一个可选的 locale 关键字,它可以用来返回其他语言/地区的年份或月份的名称。这些函数也有返回缩写名称的版本,即 dayabbrmonthabbr。首先将映射加载到 LOCALES 变量中。

julia> french_months = ["janvier", "février", "mars", "avril", "mai", "juin",
                        "juillet", "août", "septembre", "octobre", "novembre", "décembre"];

julia> french_monts_abbrev = ["janv","févr","mars","avril","mai","juin",
                              "juil","août","sept","oct","nov","déc"];

julia> french_days = ["lundi","mardi","mercredi","jeudi","vendredi","samedi","dimanche"];

julia> Dates.LOCALES["french"] = Dates.DateLocale(french_months, french_monts_abbrev, french_days, [""]);

然后可以利用上述函数进行查询。

julia> Dates.dayname(t;locale="french")
"vendredi"

julia> Dates.monthname(t;locale="french")
"janvier"

julia> Dates.monthabbr(t;locale="french")
"janv"

由于没有加载日期的缩写版本,试图使用函数 dayabbr 会出错。

julia> Dates.dayabbr(t;locale="french")
ERROR: BoundsError: attempt to access 1-element Array{String,1} at index [5]
Stacktrace:
[...]

时间类型-周期算术

在使用任何语言/日期框架时,熟悉如何处理日期-周期算术是一个很好的做法,因为有一些棘手的问题需要处理(尽管对于日-精度类型来说要少得多)。

Dates 模块的方法试图遵循简单的原则,即在做 Period 算术时尽量少改。这种方法也常被称为历法算术,或者说如果有人在对话中问你同样的计算方法,你可能会猜到。为什么要大惊小怪呢?我们举个经典的例子:把2014年1月31日加1个月。答案是什么?Javascript 会说3月3日(假设31天)。PHP 会说3月2日(假设30天)。事实上,没有正确的答案。在 Dates 模块中,它给出的结果是2月28日。它是如何计算出来的呢?我喜欢想到赌场里经典的 7-7-7 赌博游戏。

现在只要想象一下,老虎机不是 7-7-7,而是年-月-日,或者在我们的例子中,2014-01-31。当你要求在这个日期的基础上增加1个月的时候,月份槽就会递增,所以现在我们有 2014-02-31。然后检查日号是否大于新月份的最后有效日,如果大于(如上例),则日号向下调整到最后有效日(28)。这种方法的后果是什么呢?继续在我们的日期上再加一个月,2014-02-28 + Month(1) == 2014-03-28。什么?你是在期待3月的最后一天吗?不对,对不起,记得 7-7-7 的档期。尽可能少的槽位要改变,所以我们先把月份槽位递增1,2014-03-28,轰,我们就完成了,因为这是一个有效的日期。另一方面,如果我们要在原来的日期 2014-01-31 的基础上增加2个月,那么我们最终的结果是 2014-03-31,正如预期的那样。这种方法的另一个后果是,当强行进行特定的排序时,关联性会有所损失(即以不同的顺序添加东西会导致不同的结果)。比如说:

julia> (Date(2014,1,29)+Dates.Day(1)) + Dates.Month(1)
2014-02-28

julia> (Date(2014,1,29)+Dates.Month(1)) + Dates.Day(1)
2014-03-01

那是怎么回事呢?在第一行中,我们在1月29日的基础上加1天,结果是 2014-01-30;然后再加1个月,于是得到 2014-02-30,再往下调整为 2014-02-28。在第二个例子中,我们先加1个月,我们得到 2014-02-29,再往下调整为 2014-02-28,然后再加1天,结果是 2014-03-01。在这种情况下,有一个设计原则是有帮助的,那就是在存在多个 Periods 的情况下,操作将按照 Periods 的类型来排序,而不是按照它们的值或位置顺序来排序;这意味着总是先加 Year,然后加 Month,再加 Week 等。因此,以下确实会导致关联性并正好有用:

julia> Date(2014,1,29) + Dates.Day(1) + Dates.Month(1)
2014-03-01

julia> Date(2014,1,29) + Dates.Month(1) + Dates.Day(1)
2014-03-01

棘手吗?也许吧。一个无辜的 Dates 用户该怎么做?最重要的是要注意,当处理月份时,明确地强制执行某种关联性,可能会导致一些意想不到的结果,但除此之外,一切都应该按照预期工作。值得庆幸的是,在 UT 中处理时间时,日期-周期算术中的奇特情况几乎就是这样了(避免了处理夏令时、闰秒等的 “乐趣”)。

作为奖励,所有的周期算术对象都可以直接与范围一起工作。

julia> dr = Date(2014,1,29):Day(1):Date(2014,2,3)
Date("2014-01-29"):Day(1):Date("2014-02-03")

julia> collect(dr)
6-element Array{Date,1}:
 2014-01-29
 2014-01-30
 2014-01-31
 2014-02-01
 2014-02-02
 2014-02-03

julia> dr = Date(2014,1,29):Dates.Month(1):Date(2014,07,29)
Date("2014-01-29"):Month(1):Date("2014-07-29")

julia> collect(dr)
7-element Array{Date,1}:
 2014-01-29
 2014-02-28
 2014-03-29
 2014-04-29
 2014-05-29
 2014-06-29
 2014-07-29
for i in Date("2020-08-01"):Day(1):Date("2020-08-09")
           println(i)
end

2020-08-01
2020-08-02
2020-08-03
2020-08-04
2020-08-05
2020-08-06
2020-08-07
2020-08-08
2020-08-09

调整器函数

尽管日期-周期算术很方便,但经常需要在日期上进行的计算具有日历或时间的性质,而不是固定的周期数。节日就是一个很好的例子,大多数都遵循这样的规则:“纪念日 = 五月的最后一个星期一”,或者 “感恩节 = 十一月的第四个星期四”。这类时间表达式处理的是相对于日历的规则,比如本月的第一天或最后一天,下周二,或第一个和第三个星期三等。

Dates 模块通过几个方便的方法提供了调整器 API,这些方法有助于简单、简洁地表达时间规则。第一组调整器方法处理周、月、季度和年的首尾。它们每个方法都接收一个单一的 TimeType 作为输入,并返回或调整到相对于输入的所需时期的第一个或最后一个。

julia> Dates.firstdayofweek(Date(2014,7,16)) # Adjusts the input to the Monday of the input's week
2014-07-14

julia> Dates.lastdayofmonth(Date(2014,7,16)) # Adjusts to the last day of the input's month
2014-07-31

julia> Dates.lastdayofquarter(Date(2014,7,16)) # Adjusts to the last day of the input's quarter
2014-09-30

接下来的两个高阶方法 tonexttoprev,通过将一个 DateFunction 和一个起始 TimeType 作为第一个参数来概括处理时间表达式。DateFunction 只是一个函数,通常是匿名的,它接受一个单一的 TimeType 作为输入,并返回一个 Booltrue 表示满足调整标准。例如:

julia> istuesday = x->Dates.dayofweek(x) == Dates.Tuesday; # Returns true if the day of the week of x is Tuesday

julia> Dates.tonext(istuesday, Date(2014,7,13)) # 2014-07-13 is a Sunday
2014-07-15

julia> Dates.tonext(Date(2014,7,13), Dates.Tuesday) # Convenience method provided for day of the week adjustments
2014-07-15

这对于更复杂的时间表达式的 do-block 语法是很有用的。

julia> Dates.tonext(Date(2014,7,13)) do x
           # Return true on the 4th Thursday of November (Thanksgiving)
           Dates.dayofweek(x) == Dates.Thursday &&
           Dates.dayofweekofmonth(x) == 4 &&
           Dates.month(x) == Dates.November
       end
2014-11-27

Base.filter 方法可以用来获取指定范围内的所有有效日期/时刻。

# 匹兹堡街道清洁; 从 4月到11月的每第二个周二
# 日期范围从 2014-01-01 到 2015-01-01
julia> dr = Dates.Date(2014):Day(1):Dates.Date(2015);

julia> filter(dr) do x
           Dates.dayofweek(x) == Dates.Tue &&
           Dates.April <= Dates.month(x) <= Dates.Nov &&
           Dates.dayofweekofmonth(x) == 2
       end
8-element Array{Date,1}:
 2014-04-08
 2014-05-13
 2014-06-10
 2014-07-08
 2014-08-12
 2014-09-09
 2014-10-14
 2014-11-11

在 Raku 中上面的代码可以写成:

lazy my @dates = Date.new('2014-01-01') ... Date.new('2015-01-01');

.say for @dates.grep: -> $d {
    $d.day-of-week == 2 &&
    4  <= $d.month <= 11 &&
    $d.weekday-of-month == 2
}

其他的例子和测试可以在 stdlib/Dates/test/adjusters.jl 中找到。