43.7 时间转换

这些函数将时间值(see 时刻)转换为 Lisp 时间戳,或转换为日历信息,反之亦然。

许多操作系统使用 64 位有符号整数来计数秒数,可以表示遥远的过去或未来的时间。然而,有些系统限制更多。例如,使用 32 位有符号整数的老式操作系统通常仅能处理协调世界时 1901-12-13 20:45:52 至 2038-01-19 03:14:07 之间的时间。

日历转换函数即使对于公历启用之前的日期,以及对于极其遥远的过去或未来的日期(公历在这些日期极不准确,且与天文学、古生物学等科学领域的常用惯例不符,这些领域使用儒略历年长),也一律使用公历。年份从公元前 1 年开始计数,不会像传统公历那样跳过 0 年;例如,年份 −3 表示公历公元前 4 年。

Function: time-convert time form

该函数将一个时间值转换为 Lisp 时间戳。

参数 form 指定要返回的时间戳格式。 如果 form 是符号 integer,该函数返回以秒为单位的整数值。 如果 form 是正整数,它指定时钟频率,该函数返回整数对时间戳 (ticks . form)。 如果 formt,该函数将其视为适合表示该时间戳的正整数;例如,如果 timenil 且平台时间戳具有纳秒精度,则它会被视为 1000000000。 如果 formlist,该函数返回整数列表 (high low micro pico)

尽管 nil 形式的 form 目前作用与 list 相同,但这一行为计划在未来的 Emacs 版本中更改,因此需要列表时间戳的调用者应显式传入 list

如果 time 不是合法的时间值,该函数会抛出错误。 否则,如果 time 无法被精确表示,转换会向负无穷方向截断。 当 formt 时,转换总是精确的,因此不会发生截断,且返回的时钟精度不低于 time。 相比之下,尽管 float-time 也可以转换任意时间值而不抛出错误,但其结果可能不精确。see 时刻

为了效率,该函数可能返回一个与 time 满足 eq 关系的值,或者与 time 共享结构。

尽管 (time-convert nil nil) 等价于 (current-time),但后者可能稍快一些。

(setq a (time-convert nil t))
⇒ (1564826753904873156 . 1000000000)
(time-convert a 100000)
⇒ (156482675390487 . 100000)
(time-convert a 'integer)
⇒ 1564826753
(time-convert a 'list)
⇒ (23877 23681 904873 156000)
Function: decode-time &optional time zone form

该函数将一个时间值转换为日历信息。 如果你不指定 time,它会解码当前时间;同样,zone 默认为当前时区规则。see 时区规则。 操作系统会限制时间和时区值的范围。

参数 form 控制返回的 seconds 元素的格式,如下所述。 返回值是一个包含九个元素的列表,如下所示:

(seconds minutes hour day month year dow dst utcoff)

以下是各元素的含义:

seconds

分钟已过的秒数,格式如下所述。

minutes

小时已过的分钟数,0 到 59 之间的整数。

hour

一天中的小时数,0 到 23 之间的整数。

day

一个月中的日期,1 到 31 之间的整数。

month

一年中的月份,1 到 12 之间的整数。

year

年份,通常是大于 1900 的整数。

dow

星期几,0 到 6 之间的整数,0 代表星期日。

dst

如果夏令时生效则为 t,未生效则为 nil,无法获取该信息则为 −1。

utcoff

一个整数,表示与协调世界时的偏移秒数,即格林威治以东的秒数。

seconds 元素是一个非负且小于 61 的 Lisp 时间戳;除了正闰秒期间(假设操作系统支持闰秒),它始终小于 60。 如果可选参数 formtseconds 使用与 time 相同的精度; 如果 formintegerseconds 会被截断为整数。 例如,如果 time 是时间戳 (1566009571321 . 1000),在缺乏闰秒的典型系统上表示 2019-08-17 02:39:31.321 UTC, 那么 (decode-time time t t) 返回 ((31321 . 1000) 39 2 17 8 2019 6 nil 0), 而 (decode-time time t 'integer) 返回 (31 39 2 17 8 2019 6 nil 0)。 如果 form 被省略或为 nil,当前默认值为 integer,但该默认值可能在未来的 Emacs 版本中更改,因此需要特定格式的调用者应显式指定 form

Common Lisp 注意: Common Lisp 对 dowdstutcoff 的定义不同,且其二秒元素是 0 到 59 之间的整数。

要访问(或修改)日历信息中的元素,可以使用 decoded-time-seconddecoded-time-minutedecoded-time-hourdecoded-time-daydecoded-time-monthdecoded-time-yeardecoded-time-weekdaydecoded-time-dstdecoded-time-zone 这些访问器。

Function: encode-time time &rest obsolescent-arguments

该函数将 time 转换为 Lisp 时间戳。 它可以作为 decode-time 的逆函数。

通常,第一个参数是一个列表 (second minute hour day month year ignored dst zone), 它以 decode-time 的格式指定解码后的时间。 这些列表元素的含义参见 decode-time 下的表格。 特别地,dst 说明在夏令时回退导致时间戳重复时如何解释。 如果 dst 为 −1,则自动猜测 DST 值; 如果为 tnil,则返回对应 DST 值的时间戳,若不存在则抛出错误。

遗憾的是,当基于 TZDB 的时区向格林威治以西偏移时,dsttnil 无法区分重复的时间戳; 例如,当 zone 为 ‘"Europe/Volgograd"’ 时,2020-12-27 01:30 的两个标准时间戳无法区分, 因为当天 02:00 该时区标准时间从格林威治以东 4 小时改为 3 小时。 如果需要处理此类情况,可以使用数值型 zone 来区分。

第一个参数也可以是列表 (second minute hour day month year), 它等价于列表 (second minute hour day month year nil -1 nil)

作为一种过时的调用约定,该函数可以接受六个或更多参数。 前六个参数 secondminutehourdaymonthyear 指定了解码时间的大部分组件。 如果参数超过六个,最后一个 参数会被用作 zone,其他额外参数会被忽略, 因此 (apply #'encode-time (decode-time ...)) 可以正常工作。 在这种过时约定中,dst 为 −1,zone 默认为当前时区规则(see 时区规则)。 将过时调用代码现代化时,请确保现代等效列表包含 9 个元素,且 dst 元素为 −1 而非 nil

小于 100 的年份不会被特殊处理。 如果你希望它们代表 1900 年以上或 2000 年以上的年份,必须在调用 encode-time 之前自行修改。 操作系统会限制时间和时区值的范围。 但是,从纪元到不久的将来的时间戳始终受支持。

encode-time 函数大致作为 decode-time 的逆函数。 例如,你可以将后者的输出传递给前者:

(encode-time (decode-time ...))

你可以对 secondsminuteshourdaymonth 使用超出范围的值来执行简单的日期运算; 例如,日期 0 表示指定月份的前一天。 这样做时要小心,因为在某些情况下这通常会失败。 例如:

;; 尝试计算一个月后的时间。
;; 注意;这可能不会按预期工作。
(let ((time (decode-time)))
  (setf (decoded-time-month time)
        (+ (decoded-time-month time) 1))
  time)

遗憾的是,如果结果时间因月份长度差异、夏令时切换、时区更改、 缺少闰日或闰秒而无效,这段代码可能不会按预期工作。 例如,如果在 1 月 30 日执行,这段代码会生成不存在的日期 2 月 30 日, encode-time 会将其调整为 3 月初。 同样,在 2096 年 2 月 29 日上加四年会得到不存在的 2100 年 2 月 29 日; 在纽约 2022 年 3 月 13 日 01:30 加一小时会得到不存在的 02:30 时间戳, 因为当天时钟从 02:00 直接跳至 03:00。 为避免部分(并非全部)问题,你可以基于受影响单位的中间进行计算, 例如,增加月份时从每月 15 日开始。 或者,你可以使用 calendartime-date 库。