日期与时间
Lua 的标准库,提供了少量用于操作日期和时间的函数。和往常一样,他提供的只是标准 C 库中可用的功能。然而,尽管他看似简单,我们却可以利用这些基本支持,构造相当多的功能。
Lua 使用了两种日期和时间的表示法。第一种是通过通常是整数的单个数字。尽管 ISO C 没有要求,但在大多数系统上,该数字是从称为 纪元,epoch)的某个固定日期以来的秒数。特别是,在 POSIX 和 Windows 系统中,纪元均为 1970 年 1 月 1 日 0:00 UTC。
Lua 用于日期和时间的第二种表示法是表。此类 日期表,date tables,具有以下的重要字段:year
、month
、day
、hour
、min
、sec
、wday
、yday
和 isdst
。除 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
,将表表示法,转换为数字。year
、month
、day
三个字段,是必填字段。在没有提供 hour
、min
和 sec
字段时,会默认为中午 (12:00:00)。其他字段(包括 wday
和 yday
),都将被忽略。
> 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 ) |
month | 1-12 |
day | 1-31 |
hour | 0-23 |
min | 0-59 |
sec | 0-60 |
wday | 1-7 |
yday | 1-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 的含义和值。
指令 | 说明 |
---|---|
%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:请编写一个生成系统时区的函数。