日期与时间

Lua 的标准库,提供了少量用于操作日期和时间的函数。和往常一样,他提供的只是标准 C 库中可用的功能。然而,尽管他看似简单,我们却可以利用这些基本支持,构造相当多的功能。

Lua 使用了两种日期和时间的表示法。第一种是通过通常是整数的单个数字。尽管 ISO C 没有要求,但在大多数系统上,该数字是从称为 纪元,epoch)的某个固定日期以来的秒数。特别是,在 POSIX 和 Windows 系统中,纪元均为 1970 年 1 月 1 日 0:00 UTC。

Lua 用于日期和时间的第二种表示法是表。此类 日期表,date tables,具有以下的重要字段:yearmonthdayhourminsecwdayydayisdst。除 isdst 外的所有字段,都有着整数值。前六个字段的含义很明显。 wday 字段是一周中的哪一天(一为星期日); yday 字段是一年中的第几天(一为一月一日)。isdst 字段是个布尔值,若夏令时有效则为 true。例如,1998 年 9 月 16 日 23:48:10(星期三)对应了下面这个表:

{year = 1998, month = 9, day = 16, yday = 259, wday = 4,
hour = 23, min = 48, sec = 10, isdst = false}

日期表不会编码时区。由程序根据时区,来正确解释他们。

函数 os.time

在不带参数调用 os.time 时,他会返回编码为数字的当前日期和时间:

> os.time()         --> 1439653520

该日期对应着 2015 年 8 月 15 日 12:45:20。注 1在 POSIX 系统中,我们可以使用一些基本算术计算,来分解这个数字:

local date = 1439653520
local day2year = 365.242                -- 一年中的天数
local sec2hour = 60 * 60                -- 一小时的秒数
local sec2day = sec2hour * 24           -- 一天中的秒数
local sec2year = sec2day * day2year     -- 一年中的秒数

-- 年份
print(date // sec2year + 1970)        --> 2015.0

-- 小时(按 UTC)
print(date % sec2day // sec2hour)     --> 15

-- 分钟
print(date % sec2hour // 60)          --> 45

-- 秒
print(date % 60)                      --> 20

注 1:除非另有说明,我(作者)的日期,都是来自于在里约热内卢运行的一台 POSIX 系统。(这里使用了 UTC+8, Asia/Shanghai 的时区。)

我们还可以日期表,调用 os.time,将表表示法,转换为数字。yearmonthday 三个字段,是必填字段。在没有提供 hourminsec 字段时,会默认为中午 (12:00:00)。其他字段(包括 wdayyday),都将被忽略。

> os.time({year=2023, month=11, day=10, hour=12, min=45, sec=35})
1699591535
> os.time({year=1970, month=1, day=1, hour=0})
-28800
> os.time({year=1970, month=1, day=1, hour=0, sec=1})
-28799
> os.time({year=1970, month=1, day=1})
14400

请注意,其中 -28800 是以秒为单位的负八小时(时区),而 14400 则是 -28800,加上以秒为单位的 12 小时。

注意:原文 os.time({year=1970, month=1, day=1, hour=0}) 的输出为 10800。这里的输出是 UTC+8 时区的输出。

函数 os.date

尽管其名字不同,函数 os.date 却是 os.time 的某种逆向函数:他把表示日期和时间的数字,转换为某种更高级别的表示形式,可以是日期表,也可以是字符串。他的第一个参数,是描述咱们想要表示形式的 格式字符串,format string。第二个参数,是数字的日期-时间;在没有提供时,则默认为当前的日期和时间。

为生成一个日期表,我们就要使用格式字符串 "*t"。例如,调用 os.date("*t", 906000490),会返回下面的表:

> date_table = os.date("*t", 906000490)
> for k, v in pairs(date_table) do
>> print(k, v) end
min     48
year    1998
sec     10
yday    260
hour    10
wday    5
month   9
isdst   false
day     17

一般来说,对于任何有效时间 t,我们都有 os.time(os.date("*t", t)) == t

isdst 之外,结果字段是以下范围内的整数:

字段范围
year一个完整的年份(2023
month1-12
day1-31
hour0-23
min0-59
sec0-60
wday1-7
yday1-366

(秒可以达到 60,以允许闰秒。)

对于其他格式字符串,os.date 以给定时间和日期的信息,替换特定指令,而返回一个字符串的拷贝。指令是由百分号后跟一个字母组成,如下例所示:

print(os.date("a %A in %B"))                --> a Saturday in November
print(os.date("%d/%m/%Y", 906000490))       --> 17/09/1998

如果相关,表示法会遵循当前区域设置。例如,在巴西-葡萄牙语的区域设置中,%A 将得到 "terça-feira"%B 将得到 "maio"

下图 12.1,“函数 os.date 的指令” 给出了主要的指令。对于每条指令,他都显示了 1998 年 9 月 16 日(星期三)23:48:10 的含义和值。

图 12.1,函数 os.date 的指令

指令说明
%a缩写的星期几名称。(比如 "Wed"
%A完整的星期几名称。(比如 "Wednesday"
%b缩写的月份名称。(比如 "Sep"
%B完整的月份名称。(比如 "September"
%c日期和时间。(比如 "Thu Sep 17 10:48:10 1998"注 2
%d一月中的天数。("16"[01-31]
%H小时,采用 24 小时制。("10"[00-23]
%I小时,采用 12 小时制。("10"[01-12]
%j一年中的天数。("260"[001-365]
%m月份。("09"[01-12]
%M分钟。("48"[00-59]
%p"AM""PM"。("PM"
%S秒。("10"[00-60]
%w周几。("3"[0-6 = Sunday-Saturday]
%W一年中的第几周。("37"[00-53]
%x日期。("09/17/98"
%X时间。("10:48:10"
%y两位数年份。("98"
%Y完整年份。(1998
%z时区。(比如 "+0800"
%%百分号。

对于数字值,该表格还给出了其可能值的范围。以下是一些示例,展示了如何创建一些 ISO 8601 的格式:

t = 906000490
-- ISO 8601 的日期
print(os.date("%Y-%m-%d", t))           --> 1998-09-17

-- 组合了日期和时间的 ISO 8601 格式
print(os.date("%Y-%m-%dT%H:%M:%S", t))  --> 1998-09-17T10:48:10

-- ISO 8601 的序数日期
print(os.date("%Y-%j", t))              --> 1998-260

如果格式字符串以感叹号开头,那么 os.date 会将时间,解释为世界协调时间(UTC):

-- 纪元,the Epoch
> print(os.date("!%c", 0))
Thu Jan  1 00:00:00 1970
> print(os.date("%A"))
Sunday
> print(os.date("!%A"))
Saturday

如果我们不带任何参数调用 os.date,他就会使用 %c 格式,即合理格式的日期和时间信息。请注意,%x%X%c 的表示形式,会根据区域设置(locale)和系统而变化。如果咱们想要固定的表示形式,例如 dd/mm/yyyy,请使用显式的格式字符串,例如 "%d/%m/%Y"

日期时间操作

Date-Time Manipulation

os.date 创建出日期表时,其字段都会在适当的范围内。然而,当我们给 os.time 一个日期表时,该表的字段不需要标准化,its fields do not need to be normalized。这一特性,是操作日期和时间的重要工具。

举个简单的例子,假设我们想要知道,40 天后的日期。按照以下方式,咱们就可以计算出该日期:

t = os.date("*t")           -- 获取当前日期
print(os.date("%Y/%m/%d", os.time(t)))        --> 2023/11/12
t.day = t.day + 40
print(os.date("%Y/%m/%d", os.time(t)))        --> 2023/12/22

如果我们将数字时间,转换回表,我们就会得到该日期时间的规范化版本:

t = os.date("*t")
print(t.day, t.month)               --> 12      11
t.day = t.day - 40
print(t.day, t.month)               --> -28     11
t = os.date("*t", os.time(t))
print(t.day, t.month)               --> 3       10

在此示例中,11 月 -28 日,就已被标准化为 10 月 3 日,即 11 月 12 日之前 40 天。

在大多数系统中,我们也可以在数字时间上,加上或减去 3456000(40 天,以秒为单位)。但是,C 标准不保证此操作的正确性,因为他未要求数字时间,来表示自某个纪元的秒数。此外,如果我们想添加几个月,而不是几天,直接操作秒就会出现问题,因为不同月份有不同的持续时间。相比之下,标准化(归一化)方法,the normalization method,就不存在下面这些问题:

t = os.date("*t")           -- 获取当前日期
print(os.date("%Y/%m/%d", os.time(t)))        --> 2023/11/12
t.month = t.month + 6       -- 此后 6 个月
print(os.date("%Y/%m/%d", os.time(t)))        --> 2024/05/12

在操作日期时,我们必须要小心。标准化会以明显方式运作,但他可能会有着一些不明显的后果。例如,如果我们要计算 3 月 31 日之后的一个月,则会得到 4 月 31 日,该日期会被标准化为 5 月 1 日(4 月 30 日后一天)。这听起来很自然。然而,如果我们从该结果(5 月 1 日)往前推一个月,我们就会到 4 月 1 日,而不是原来的 3 月 31 日。请注意,这种不匹配是我们日历运作方式的结果;和Lua 没有任何关系。

要计算两个时间之间的差异,有着函数 os.difftime。他会返回两个给定数字时间之间的差异(以秒为单位)。对于大多数系统来说,这种差异正是从一个时间,减去另一时间的结果。然而,与减法不同,os.difftime 的行为,在任何系统中,都是有保证的。下面这个个示例,计算了 Lua 5.2 和 Lua 5.3 版本之间,所经过的天数:

local t5_3 = os.time({year=2015, month=1, day=12})
local t5_2 = os.time({year=2011, month=12, day=16})
local d = os.difftime(t5_3, t5_2)
print(d // (24 * 3600))         --> 1123.0

使用 difftime,我们可以将日期,表示为自任意纪元以来的秒数:

myepoch = os.time {year = 2000, month = 1, day = 1, hour = 0}
now = os.time {year = 2023, month = 11, day = 12}
print(os.difftime(now, myepoch))       --> 753105600.0

使用标准化,就可以很容易地,将秒数转换回合法的数字时间:我们创建出一个带有纪元的表,并将其秒数,设置为我们要转换的数字,如下一个示例所示。

T = {year = 2000, month = 1, day = 1, hour = 0}
T.sec = 753105600
print(os.date("%d/%m/%Y", os.time(T)))  --> 12/11/2023

我们还可以使用 os.difftime,来计算出某段代码的运行时间。然而,对于此任务,最好使用 os.clock。函数 os.clock 会返回程序使用 CPU 时间的秒数。其典型用途,是对一段代码进行基准测试:

local x = os.clock()
local s = 0
for i = 1, 100000 do s = s+ i end
print(string.format("经过时间:%.8f\n", os.clock() - x))
    --> 经过时间:0.00035900

os.time 不同,os.clock 通常具有亚秒精度,sub-second precision,因此其结果是浮点数。确切的精度,取决于平台;在 POSIX 系统中,通常为一微秒(注:因此对 os.clock 的结果,使用格式字串(指令) %.6f 较好)。

练习

练习 12.1:请编写一个返回恰好是给定日期时间,一个月后日期时间的函数。 (假设日期时间是数字编码。)

练习12.2:请编写一个返回给定日期的星期几(编码为整数,其中一为星期日)的函数。

练习 12.3:请编写一个取日期时间(编码为数字),并返回自当天开始以来,所经过秒数的函数。

练习 12.4:编写一个取某个年份,并返回其第一个星期五日期的函数。

练习 12.5:请编写一个计算两个给定日期之间,完整天数的函数。

练习 12.6:请编写一个计算两个给定日期之间,完整月数的函数。

练习 12.7:请问往给定日期加一个月和一天,与加一天和一个月的结果,是否相同?

练习12.8:请编写一个生成系统时区的函数。

Last change: 2023-11-24, commit: fedeca9