这篇文章首先将介绍 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 | > os.time() |
如果以一个日期作为参数调用 os.time()
,则会返回该表中所描述日期和时间对应的数字。year、month、day 是必须的,hour、min、sec 字段如果没有提供,则默认为 12:00:00
,其余字段会被忽略:
1 | > os.time({year = 2023, month = 5, day= 11, hour = 10, min = 0, sec = 0}) |
函数 os.date
函数 os.date
可以将一个表示日期和时间的数字转换为某些高级表示形式,要么是日期表要么是字符串。该函数第一个参数是描述期望表示形式的格式化字符串,第二个参数是数字形式的日期和时间(不提供则默认为当前时间)。
- 要生成日期表,使用格式化字符串
*t
。对于任何有效时间,os.time(os.date("*t",t))== t
均有效 - 对于其他格式的字符串,则会将日期格式化为一个字符串。该字符串是根据指定的时间对特定的指示符进行了替换的结果。所有指示符均以
%
开头。如果格式化字符串以感叹号开头,那么os.date
会以 UTC 格式对其进行解析。如果不带任何参数调用os.date()
,会以格式%c
输出当前时间。
1 | > t = os.date("*t") |
日期和时间处理
当 os.date
创建日期表时,该表的所有字段均在有效范围内。当给函数 os.time
传入一个日期表时,其中的字段并不需要归一化。该特性对日期和时间的处理非常重要。
1 | > t = os.date("*t") |
只需要把数字表示的时间转换成日期表,又能重新得到日期和时间的归一化形式:
1 | > t = os.date("*t") |
os.difftime
用来计算两个时间之间的差值,该函数以秒为单位返回两个指定数字形式表示的时间的差值:
1 | > t1 = os.time({year=2023, month=5, day=11}) |
利用归一化的处理,也可以很容易将用秒表示的时间转换为合法的、数字形式表示的时间:
1 | > t = {year=2023, month=5, day=11} |
os.clock()
可以返回程序消耗的 CPU 时间(单位 s),os.clock()
通常具有比秒更高的精度,因此其返回一个浮点数。具体的精度与平台相关,在 POSIX 中通常是 1ms。其在性能测试的典型用法如下:
1 | > x = os.clock() |
位和字节
Lua 中的字符串可以包含任意字节,并且几乎所有能够处理字符串的库函数也能处理字节。以此为基础,Lua5.3 引入了用于操作二进制数据的额外机制:除了整型数之外,该版本还引入了位操作以及用于打包/解包二进制数据的函数。
位运算
Lua5.3 开始提供了针对数据类型的一组标准位运算符。与算术运算符不同,位运算只能用于整型数。位运算符包括:&
、|
、~
、>>
、<<
和一元运算符 ~(按位取反)。
1 | > string.format("%x", 0xff & 0xabcd) |
所有的位运算符都针对构成一个整数的所有位,在标准 Lua 中,即 64 位。移位操作都会用 0 填充空出的位,该行为也称为逻辑移位,Lua 没有提供算术移位(即用符号位来填充空出的位)。
移位数是负数表示向相反方向移位,例如 a >> n
等价于 a << -n
。另外如果移位数大于整型表示位数,结果为 0。
无符号整数
整型表示使用一个比特位来存储符号位。尽管 Lua 不显示支持无符号整型数,但是处理无符号整型数并不难。在 Lua 中可以直接写出比 2^63 -1
大的常量,但此时会当成浮点数。
1 | > x = 13835058055282163712 |
Lua5.3 提供了函数 math.ult
来把两个整数都当成无符号比较:
1 | > 0x7FFFFFFFFFFFFFFF < 0x8000000000000000 |
打包和解包二进制数据
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 | > s = string.pack("iii", 3, -27, 450) |
1 | > s = string.pack(">i4", 0x0a0b0c0d) |
二进制文件
io.input
和 io.output
总是以文本方式打开文件,如果想以二进制模式打开文件,在 io.open()
中模式字符串使用字母 b
。在读取二进制数据时,要么以 a
模式来读取整个文件,要么以 n
模式来读取 n 个字节。
如下程序以十六进制内容输出二进制文件:
1 | local f = assert(io.open(arg[1], "rb")) |