Lua日期与时间操作详解:os.time与os.date的用法 | 南锋

南锋

南奔万里空,脱死锋镝余

Lua日期与时间操作详解:os.time与os.date的用法

Lua语言的标准库提供了两个用于操作日期和时间的函数,这两个函数在C语言标准库中也存在,提供的是同样的功能。虽然这两个函数看上去很简单,但依旧可以基于这些简单的功能完成很多复杂的工作。

Lua语言针对日期和时间使用两种表示方式。第1中表示方式是一个数字,这个数字通常是一个整型数。尽管并非IOS C所必需的,但在大多数系统中这个数字时自一个被称为纪元的固定日期后至今的秒数。特别地,在POSIX和Windows系统中国这个固定日期均是Jan 01,1970,0:00 UTC。
Lua语言针对日期和时间提供的第2中表示方式是一个表。日期表具有以下几个重要的字段:year、month、day、hour、min、sec、wday、yady和isdst,除isdst以外的所有字段均为整型值。前6个字段的含义非常明显,而wday字段表示本周中的第几天(第1天为星期天);yday表示当年中的第几天(第1天是1月1日);isdst字段表示布尔类型,如果使用夏时令则为真。例如,Sep 16,1998,23:48:10(星期三)对应的表是:

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

日期表中不包括时区,程序需要负责结合相应的时区对其正确解析。

函数os.time

不带任何参数调用函数os.time,会以数字形式返回当前的日期和时间:

1
os.time()	   -- 1587123725

对应的时间是April 17,2020,19:42:05。在一个POSIX系统中,可以使用一些基本的数字运算分离这个数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local date = 1587123725
local day2year = 265.242 一年的天数
local sec2hour = 60 * 60 一小时的秒数
local sec2day = sec2hour * 24 一天的秒数
local sec2year = sec2day * day2year 一年的秒数


print(date // sec2year + 1970)

小时
print(date % sec2day // sec2hour)

分钟
print(date % sec2hour // 60)


print(date % 60)

如果以一个日期表作为参数调用os.time,那么改函数会返回该表中所描述日期和时间对应的数字。year、month和day字段是必需的,hour、min和sec字段如果没有提供的话则默认为12:00:00,其余字段则会被忽略。

1
2
3
4
os.time({year = 2020,month = 4,day = 17,hour = 19,min = 42,sec = 5})		 1587123725
os.time({year = 1970,month = 1,day = 1, hour = 0}) 10800
os.time({year = 1970,month = 1,day = 1, hour = 0,sec = 1}) 10801
os.time({year = 1970,month = 1,day = 1}) 54000

请注意,10800是3个小时的秒数,54000则是10800加上12个小时的秒数。

函数os.date

函数os.date在一定程度上是函数os.time的反函数,它可以将一个表示日期和时间的数字转换为某些高级的表示形式,要么是日期表要么是字符串。该函数的第1个参数是描述期望表示形式的格式化字符串,第2个参数是数字形式的日期和时间。
要生成一个日期表,可以使用格式化字符串”*t”。例如,调用函数os.date(“*t”,1587123725)会返回下列表:

1
{year = 2020,month = 9,day = 16,yday = 108,wday = 6,hour = 19,min = 42,sec = 5,isdst = false}

大致上,对于任何有效的时间t,os.time(os.date(“*t”,t)) == t均成立。
除了isdst,结果中的其余字段均为整型数且范围分别是:


year 一整年
month 112
day 1
31
hour 023
min 0
59
sec 060
wday 1
7
yday 1~366


对于其他格式化字符串,函数os.date会将日期格式化为一个字符串,该字符串是根据指定的时间和日期信息对特定的指示符进行了替换的结果。所有的指示符都以百分号开头紧跟一个字母,例如:

1
2
print(os.date("a %A in %B"))
print(os.date("%d/%m/%Y",1587123725)) 17/4/2020

所有的表现形式取决于当前的区域设置。
下表列出了主要的指示符。


%a星期几的简写
%A星期几的全名
%b月份的简写
%B月份的全名
%c日期和时间
%d一个月中的第几天[0131]
%H24小时制中的小时数[00
23]
%I12小时制中的小时数[0112]
%j一年中的第几天[001
365]
%m月份[112]
%M分钟[0
59]
%p”am”或”pm”
%S秒数[060]
%w星期[0
6 = Sunday ~ Saturday]
%W一年中的第几周[053]
%x日期
%X时间
%y两位数的年份[0
99]
%Y完整的年份
%z时区
%%百分号


对于数值,表中也给出了它们的有效范围。以下是一些演示如何创建IOS 8601格式日期和时间的示例:
用函数os.date(“*t”,1587123725)会返回下列表:

1
2
3
4
5
6
7
8
{year = 2020,month = 9,day = 16,yday = 108,wday = 6,hour = 19,min = 42,sec = 5,isdst = false}
```lua
t = 1587123725 -- IOS 8601格式的日期
print(os.date("%Y-%m-%d",t)) 2020-04-17
IOS 8601格式的日期和时间
print(os.date("%Y-%m-%dT%H:%M:%S",t)) 2020-04-17T19:42:05
IOS 8601格式的序数日期
print(os.date("%Y-%j",t))

如果格式化字符串以叹号开头,那么函数os.date会以UTC格式对其进行解析:

1
2
 纪元
print(os.date("!%c",0)) Zhu Jan 1 00:00:00 1970

如果不带任何参数调用函数os.date,那么该函数会使用格式%c,即以一种合理的格式表示日期和时间信息。请注意,%x、%X和%c会根据不同的区域和系统而发生变化。如果需要诸如dd/mm/yyyy这样的固定表示形式,那么就必须显示地使用诸如”%d/%m/%Y”这样的格式化字符串。

日期和时间处理

当函数os.date创建日期表时,该表的所有字段均在有效的范围内。当我们给函数os.time传入一个日期表时,其中的字段并不需要归一化。这个特行对于日期和时间处理非常重要。
举一个简单的例子,假设想知道从当前向后数40天的日期,那么可以使用如下的代码进行计算:

1
2
3
4
t = os.date("*t")		
print(os.date("%Y/%m/%d",os.time(t))) 2020/04/17
t.day = t.day + 40
print(os.date("%Y/%m/%d",os.time(t))) 2020/05/27

如果我们把数字表示的时间转换成日期表,那么就能得到日期和时间的归一化形式:

1
2
3
4
5
6
t = os.date("*t")
print(t.day,t.month) 26 2
t.day = t.day - 40
print(t.day,t.month) -14 2
t = os.date("*t",os.time(t))
print(t.day,t.month) 17 1

在大多数系统中,也可以对数字形式的时间增加或减少34356000(40天对应的秒数)。不过,由于标准C并不要求数值表示的时间是从纪元开始的,因此标准C并不保证这种操作的正确性。此外,如果我们想增加的是月份而非天数,由于不同的月份具有不同的天数,那么直接操作秒数就会有问题。而以归一化的方式处理则没有这些问题:

1
2
3
4
t = os.date("*t")	-- 获取当前时间
print(os.date("%Y/%m/%d".os.time(t))) 2020/04/17
t.month = t.month + 6 从当天开始往后6个月
print(os.date("%Y/%m/%d".os.time(t))) 2020/10/17

在操作日期时,我们必须要小心。虽然归一化是显而易见的方式进行的,但是也可能会有一些不明显的后果。例如,如果计算March 31之后的一个月,将会得到April 31,而实际上应该被归一化为May 1。尽管这听上去很自然,但实际上如果从结果(May 1)中减去一个月,得到的确实April 1而不是原来的March 31。请注意,这种不一致是日历机制导致的结果,与Lua语言无关。
函数os.difftime用来计算两个时间之间的差值,该函数以秒为单位返回两个指定数字形式表示的时间的差值。对于大多数系统而言,这个差值就是一个时间相对于另一个时间的减法结果。但是,与减法不同,函数os.difftime的行为在任何系统中都是确定的。以下示例计算了Lua5.2和Lua5.3发布时间之间间隔的天数:

1
2
3
4
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可以获取指定日期和相对任意时刻的秒数:

1
2
3
myepoch = os.time{year = 2000,month = 1,day = 1,hour = 0}
now = os.time{year = 2015,month = 11,day = 20}
os.difftime(now,myepoch) 501336000.0

通过归一化,可以很容易地将用秒表示的时间转换为合法的数字形式表示的时间,即我们以创建一个带有开始时刻的日期表并将日期表中的秒数设置为想要转换的数字。例如:

1
2
3
T = {year = 2000,month = 1,day = 1,hour = 0}
t.sec = 501336000
os.date("%d/%m/%Y",os.time(T)) 20/11/2015

我们还可以使用函数os.difftime来计算一段代码的执行时间。不过,对于这个需求更好的方式是使用函数os.clock,该函数会返回程序消耗的CPU时间(单位是秒)。函数os.clock在性能测试中的典型用法如:

1
2
3
4
local x = os.clock()
local s = 0
for i = 1,100000 do s = s + i end
print(string.format("elapsed time: %.2f\n",os.clock() - x))

与函数os.time不同,函数os.clock通常具有比秒更高的精度,因此返回值为一个浮点数。具体的精度与平台相关,在POSIX系统中通常是1毫秒。

@[toc]
Lua语言的标准库提供了两个用于操作日期和时间的函数,这两个函数在C语言标准库中也存在,提供的是同样的功能。虽然这两个函数看上去很简单,但依旧可以基于这些简单的功能完成很多复杂的工作。
Lua语言针对日期和时间使用两种表示方式。第1中表示方式是一个数字,这个数字通常是一个整型数。尽管并非IOS C所必需的,但在大多数系统中这个数字时自一个被称为纪元的固定日期后至今的秒数。特别地,在POSIX和Windows系统中国这个固定日期均是Jan 01,1970,0:00 UTC。
Lua语言针对日期和时间提供的第2中表示方式是一个表。日期表具有以下几个重要的字段:year、month、day、hour、min、sec、wday、yady和isdst,除isdst以外的所有字段均为整型值。前6个字段的含义非常明显,而wday字段表示本周中的第几天(第1天为星期天);yday表示当年中的第几天(第1天是1月1日);isdst字段表示布尔类型,如果使用夏时令则为真。例如,Sep 16,1998,23:48:10(星期三)对应的表是:

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

日期表中不包括时区,程序需要负责结合相应的时区对其正确解析。

函数os.time

不带任何参数调用函数os.time,会以数字形式返回当前的日期和时间:

1
os.time()	    1587123725

对应的时间是April 17,2020,19:42:05。在一个POSIX系统中,可以使用一些基本的数字运算分离这个数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local date = 1587123725
local day2year = 265.242 一年的天数
local sec2hour = 60 * 60 一小时的秒数
local sec2day = sec2hour * 24 一天的秒数
local sec2year = sec2day * day2year 一年的秒数


print(date // sec2year + 1970)

小时
print(date % sec2day // sec2hour)

分钟
print(date % sec2hour // 60)


print(date % 60)

如果以一个日期表作为参数调用os.time,那么改函数会返回该表中所描述日期和时间对应的数字。year、month和day字段是必需的,hour、min和sec字段如果没有提供的话则默认为12:00:00,其余字段则会被忽略。

1
2
3
4
os.time({year = 2020,month = 4,day = 17,hour = 19,min = 42,sec = 5})		 1587123725
os.time({year = 1970,month = 1,day = 1, hour = 0}) 10800
os.time({year = 1970,month = 1,day = 1, hour = 0,sec = 1}) 10801
os.time({year = 1970,month = 1,day = 1}) 54000

请注意,10800是3个小时的秒数,54000则是10800加上12个小时的秒数。

函数os.date

函数os.date在一定程度上是函数os.time的反函数,它可以将一个表示日期和时间的数字转换为某些高级的表示形式,要么是日期表要么是字符串。该函数的第1个参数是描述期望表示形式的格式化字符串,第2个参数是数字形式的日期和时间。
要生成一个日期表,可以使用格式化字符串”*t”。例如,调用函数os.date(“*t”,1587123725)会返回下列表:

1
{year = 2020,month = 9,day = 16,yday = 108,wday = 6,hour = 19,min = 42,sec = 5,isdst = false}

大致上,对于任何有效的时间t,os.time(os.date(“*t”,t)) == t均成立。
除了isdst,结果中的其余字段均为整型数且范围分别是:


year 一整年
month 112
day 1
31
hour 023
min 0
59
sec 060
wday 1
7
yday 1~366


对于其他格式化字符串,函数os.date会将日期格式化为一个字符串,该字符串是根据指定的时间和日期信息对特定的指示符进行了替换的结果。所有的指示符都以百分号开头紧跟一个字母,例如:

1
2
print(os.date("a %A in %B"))
print(os.date("%d/%m/%Y",1587123725)) 17/4/2020

所有的表现形式取决于当前的区域设置。
下表列出了主要的指示符。


%a星期几的简写
%A星期几的全名
%b月份的简写
%B月份的全名
%c日期和时间
%d一个月中的第几天[0131]
%H24小时制中的小时数[00
23]
%I12小时制中的小时数[0112]
%j一年中的第几天[001
365]
%m月份[112]
%M分钟[0
59]
%p”am”或”pm”
%S秒数[060]
%w星期[0
6 = Sunday ~ Saturday]
%W一年中的第几周[053]
%x日期
%X时间
%y两位数的年份[0
99]
%Y完整的年份
%z时区
%%百分号


对于数值,表中也给出了它们的有效范围。以下是一些演示如何创建IOS 8601格式日期和时间的示例:
用函数os.date(“*t”,1587123725)会返回下列表:

1
2
3
4
5
6
7
8
{year = 2020,month = 9,day = 16,yday = 108,wday = 6,hour = 19,min = 42,sec = 5,isdst = false}
```lua
t = 1587123725 IOS 8601格式的日期
print(os.date("%Y-%m-%d",t)) 2020-04-17
IOS 8601格式的日期和时间
print(os.date("%Y-%m-%dT%H:%M:%S",t)) 2020-04-17T19:42:05
IOS 8601格式的序数日期
print(os.date("%Y-%j",t))

如果格式化字符串以叹号开头,那么函数os.date会以UTC格式对其进行解析:

1
2
 纪元
print(os.date("!%c",0)) Zhu Jan 1 00:00:00 1970

如果不带任何参数调用函数os.date,那么该函数会使用格式%c,即以一种合理的格式表示日期和时间信息。请注意,%x、%X和%c会根据不同的区域和系统而发生变化。如果需要诸如dd/mm/yyyy这样的固定表示形式,那么就必须显示地使用诸如”%d/%m/%Y”这样的格式化字符串。

日期和时间处理

当函数os.date创建日期表时,该表的所有字段均在有效的范围内。当我们给函数os.time传入一个日期表时,其中的字段并不需要归一化。这个特行对于日期和时间处理非常重要。
举一个简单的例子,假设想知道从当前向后数40天的日期,那么可以使用如下的代码进行计算:

1
2
3
4
t = os.date("*t")		
print(os.date("%Y/%m/%d",os.time(t))) 2020/04/17
t.day = t.day + 40
print(os.date("%Y/%m/%d",os.time(t))) 2020/05/27

如果我们把数字表示的时间转换成日期表,那么就能得到日期和时间的归一化形式:

1
2
3
4
5
6
t = os.date("*t")
print(t.day,t.month) 26 2
t.day = t.day - 40
print(t.day,t.month) -14 2
t = os.date("*t",os.time(t))
print(t.day,t.month) 17 1

在大多数系统中,也可以对数字形式的时间增加或减少34356000(40天对应的秒数)。不过,由于标准C并不要求数值表示的时间是从纪元开始的,因此标准C并不保证这种操作的正确性。此外,如果我们想增加的是月份而非天数,由于不同的月份具有不同的天数,那么直接操作秒数就会有问题。而以归一化的方式处理则没有这些问题:

1
2
3
4
t = os.date("*t")	 获取当前时间
print(os.date("%Y/%m/%d".os.time(t))) 2020/04/17
t.month = t.month + 6 从当天开始往后6个月
print(os.date("%Y/%m/%d".os.time(t))) 2020/10/17

在操作日期时,我们必须要小心。虽然归一化是显而易见的方式进行的,但是也可能会有一些不明显的后果。例如,如果计算March 31之后的一个月,将会得到April 31,而实际上应该被归一化为May 1。尽管这听上去很自然,但实际上如果从结果(May 1)中减去一个月,得到的确实April 1而不是原来的March 31。请注意,这种不一致是日历机制导致的结果,与Lua语言无关。
函数os.difftime用来计算两个时间之间的差值,该函数以秒为单位返回两个指定数字形式表示的时间的差值。对于大多数系统而言,这个差值就是一个时间相对于另一个时间的减法结果。但是,与减法不同,函数os.difftime的行为在任何系统中都是确定的。以下示例计算了Lua5.2和Lua5.3发布时间之间间隔的天数:

1
2
3
4
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可以获取指定日期和相对任意时刻的秒数:

1
2
3
myepoch = os.time{year = 2000,month = 1,day = 1,hour = 0}
now = os.time{year = 2015,month = 11,day = 20}
os.difftime(now,myepoch) 501336000.0

通过归一化,可以很容易地将用秒表示的时间转换为合法的数字形式表示的时间,即我们以创建一个带有开始时刻的日期表并将日期表中的秒数设置为想要转换的数字。例如:

1
2
3
T = {year = 2000,month = 1,day = 1,hour = 0}
t.sec = 501336000
os.date("%d/%m/%Y",os.time(T)) 20/11/2015

我们还可以使用函数os.difftime来计算一段代码的执行时间。不过,对于这个需求更好的方式是使用函数os.clock,该函数会返回程序消耗的CPU时间(单位是秒)。函数os.clock在性能测试中的典型用法如:

1
2
3
4
local x = os.clock()
local s = 0
for i = 1,100000 do s = s + i end
print(string.format("elapsed time: %.2f\n",os.clock() - x))

与函数os.time不同,函数os.clock通常具有比秒更高的精度,因此返回值为一个浮点数。具体的精度与平台相关,在POSIX系统中通常是1毫秒。

+