0%

《lua 程序设计》读书笔记(6):日期时间 & 位和字节

这篇文章首先将介绍 Lua 标准库中操作日期时间相关的接口,之后则将介绍 Lua 中的的位/字节操作相关的 API。

日期时间

Lua 标准库提供了两个用于操作日期和时间的函数,这两个函数在 C 语言标准库中也存在,提供相同的功能。Lua 针对日期和时间使用两种表示方式:

  • 方式 1 使用一个数字(整型数),在大多数系统中这个数字是一个被称为纪元(epoch)的固定日期(通常是 1970.1.1 0:00 UTC)至今的秒数
  • 方式 2 使用一个表,日期表具有几个重要字段:year、month、day、hour、min、sec、wday、yday 和 isdst。日期表中不包含时区,程序需要结合相应的时区对其进行正确解析

函数 os.time

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

1
2
> os.time()
1683771981

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

1
2
3
4
> os.time({year = 2023, month = 5, day= 11, hour = 10, min = 0, sec = 0})
1683770400
> os.time({year = 1970, month = 1, day= 1})
14400

函数 os.date

函数 os.date 可以将一个表示日期和时间的数字转换为某些高级表示形式,要么是日期表要么是字符串。该函数第一个参数是描述期望表示形式的格式化字符串,第二个参数是数字形式的日期和时间(不提供则默认为当前时间)。

  • 要生成日期表,使用格式化字符串 *t。对于任何有效时间,os.time(os.date("*t",t))== t 均有效
  • 对于其他格式的字符串,则会将日期格式化为一个字符串。该字符串是根据指定的时间对特定的指示符进行了替换的结果。所有指示符均以 % 开头。如果格式化字符串以感叹号开头,那么 os.date 会以 UTC 格式对其进行解析。如果不带任何参数调用 os.date(),会以格式 %c 输出当前时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> t = os.date("*t")
> for k, v in pairs(t) do
>> print(k, v)
>> end
isdst false
yday 131
day 11
year 2023
hour 10
min 40
month 5
wday 5
sec 18

> print(os.date("%Y-%m-%dT%H:%M:%S"))
2023-05-11T10:48:28
> print(os.date("%c"))
Thu May 11 10:49:15 2023
> print(os.date("!%c"))
Thu May 11 02:51:49 2023
> os.date()
Thu May 11 10:53:45 2023

日期和时间处理

os.date 创建日期表时,该表的所有字段均在有效范围内。当给函数 os.time 传入一个日期表时,其中的字段并不需要归一化。该特性对日期和时间的处理非常重要。

1
2
3
4
5
>  t = os.date("*t")
> print(os.date("%c", os.time(t)))
Thu May 11 11:16:22 2023
> print(os.date("%c", os.time(t)))
Tue Jun 20 11:16:22 2023

只需要把数字表示的时间转换成日期表,又能重新得到日期和时间的归一化形式:

1
2
3
4
5
6
7
8
9
> t = os.date("*t")
> print(t.day, t.month)
11 5
> t.day = t.day - 40
> print(t.day, t.month)
-29 5
> t = os.date("*t", os.time(t))
> print(t.day, t.month)
1 4

os.difftime 用来计算两个时间之间的差值,该函数以秒为单位返回两个指定数字形式表示的时间的差值:

1
2
3
4
> t1 = os.time({year=2023, month=5, day=11})
> t2 = os.time({year=2023, month=5, day=10})
> os.difftime(t1, t2)
86400.0

利用归一化的处理,也可以很容易将用秒表示的时间转换为合法的、数字形式表示的时间:

1
2
3
4
5
> t = {year=2023, month=5, day=11}
> t.sec = 360000
> os.date("%c", t)
> os.date("%c", os.time(t))
Mon May 15 16:00:00 2023

os.clock() 可以返回程序消耗的 CPU 时间(单位 s),os.clock() 通常具有比秒更高的精度,因此其返回一个浮点数。具体的精度与平台相关,在 POSIX 中通常是 1ms。其在性能测试的典型用法如下:

1
2
3
4
5
> x = os.clock()
> s = 0
> for i = 1, 1000000 do s = s + i end
> string.format("time: %.2f\n", os.clock() - x)
time: 0.02

位和字节

Lua 中的字符串可以包含任意字节,并且几乎所有能够处理字符串的库函数也能处理字节。以此为基础,Lua5.3 引入了用于操作二进制数据的额外机制:除了整型数之外,该版本还引入了位操作以及用于打包/解包二进制数据的函数。

位运算

Lua5.3 开始提供了针对数据类型的一组标准位运算符。与算术运算符不同,位运算只能用于整型数。位运算符包括:&|~>><< 和一元运算符 ~(按位取反)。

1
2
3
4
> string.format("%x", 0xff & 0xabcd)
cd
> string.format("%x", 0xff | 0xabcd)
abff

所有的位运算符都针对构成一个整数的所有位,在标准 Lua 中,即 64 位。移位操作都会用 0 填充空出的位,该行为也称为逻辑移位,Lua 没有提供算术移位(即用符号位来填充空出的位)。

移位数是负数表示向相反方向移位,例如 a >> n 等价于 a << -n。另外如果移位数大于整型表示位数,结果为 0。

无符号整数

整型表示使用一个比特位来存储符号位。尽管 Lua 不显示支持无符号整型数,但是处理无符号整型数并不难。在 Lua 中可以直接写出比 2^63 -1 大的常量,但此时会当成浮点数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> x = 13835058055282163712
> math.type(x)
float
> x = 13835058055282163712
> string.format("%u", x)
stdin:1: bad argument #2 to 'format' (number has no integer representation)
stack traceback:
[C]: in function 'string.format'
stdin:1: in main chunk
[C]: in ?

> string.format("%u", 3 << 62)
13835058055282163712
> string.format("0x%X", 3 << 62)
0xC000000000000000

Lua5.3 提供了函数 math.ult 来把两个整数都当成无符号比较:

1
2
3
4
> 0x7FFFFFFFFFFFFFFF < 0x8000000000000000
false
> math.ult(0x7FFFFFFFFFFFFFFF, 0x8000000000000000)
true

打包和解包二进制数据

Lua5.3 还引入了一个在二进制数和基本类型值(数值和字符串类型)之间进行转换的函数。

  • string.pack 会将值打包为二进制字符串,而函数 string.unpack 则从字符串中提取这些值。这些函数的第一个参数是格式化字符串,用于描述如何打包数据。
  • 对于编码一个整型数而言有几种选项,每一种对应了一种整型的大小:b(char)、h(short)、i(int)、l(long) 和 j(代表 Lua 中整型数的大小)。要是使用固定的、与机器无关的大小,可以在选项 i 之后加上一个 1-16 的数。每一个针对整型数的选项都有一个对应的大写版本,对应相应大小的无符号整型。
  • 可以使用 3 种表示形式打包字符串
    • \0 结尾的字符串:使用选项 \z
    • 定长字符串:使用选项 cn,n 为被打包字符串的字节数
    • 显示长度的字符串:在存储时会在字符串前加上该字符串的长度,使用选项 sn,n 为用于保存字符串长度的无符号整型数的大小。如果单纯使用选项 s,此时字符串长度会被以足够容纳任何字符串长度的 size_t 类型保存。
  • 对于浮点数,有 3 种选项:f 用于单精度浮点数、d 用于双精度浮点数、n 用于 Lua 浮点数
  • 格式字符串也有用来控制大小端模式和二进制数据对齐的选项。默认情况下,格式使用的是机器原生的大小端模式,选项 > 把所有后续编码转换成大端模式(网络序),而 < 则改为小端模式,而 = 则使用机器默认的原生大小端模式
  • 对于对齐而言,!n 的作用是:如果数据比 n 小,那么对齐到其自身大小上,否则对齐到 n 上。通过在结果字符串到达合适索引值前增加 0 的方式实现对齐
  • 所有的格式化字符串默认带有前缀 =!1,即表示使用默认的大小端模式且不对齐(每个索引都是 1 的倍数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> s = string.pack("iii", 3, -27, 450)
> #s
12
> string.unpack("iii", s)
3 -27 450 13

> s = "\xFF"
> string.unpack("b", s)
-1 2
> string.unpack("B", s)
255 2

> s = string.pack("s1", "hello")
> for i = 1, #s do print((string.unpack("B", s, i))) end
5
104
101
108
108
111
1
2
3
4
5
6
7
8
9
10
11
12
13
> s = string.pack(">i4", 0x0a0b0c0d)
> for i = 1, #s do print((string.unpack("B", s, i))) end
10
11
12
13

> s = string.pack("<i4", 0x0a0b0c0d)
> for i = 1, #s do print((string.unpack("B", s, i))) end
13
12
11
10

二进制文件

io.inputio.output 总是以文本方式打开文件,如果想以二进制模式打开文件,在 io.open() 中模式字符串使用字母 b。在读取二进制数据时,要么以 a 模式来读取整个文件,要么以 n 模式来读取 n 个字节。

如下程序以十六进制内容输出二进制文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
local f = assert(io.open(arg[1], "rb"))
local blocksize = 16

for bytes in f:lines(blocksize) do
for i = 1, #bytes do
local b = string.unpack("B", bytes, i)
io.write(string.format("%02X ", b))
end
io .write(string.rep(" ", blocksize - #bytes))

bytes = string.gsub(bytes, "%c", ".")
io.write(" ", bytes, "\n")
end