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
访问器函数
因为 Date 和 DateTime 类型被存储为单个 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
dayname 和 monthname 方法也可以使用一个可选的 locale
关键字,它可以用来返回其他语言/地区的年份或月份的名称。这些函数也有返回缩写名称的版本,即 dayabbr 和 monthabbr。首先将映射加载到 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
接下来的两个高阶方法 tonext 和 toprev,通过将一个 DateFunction
和一个起始 TimeType 作为第一个参数来概括处理时间表达式。DateFunction
只是一个函数,通常是匿名的,它接受一个单一的 TimeType 作为输入,并返回一个 Bool,true
表示满足调整标准。例如:
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 中找到。