lua的os.time()受时区影响
问题
最近在项目中发现os.time()返回的结果里把输入当作本地时间进行处理,而不是当作UTC的时间处理。
例如这样一行代码:
local tm = os.time({year=1970, month=1, day=1, hour = 8});
print(tm)
预期返回是8*60*60,但是输出tm会得到0。具体原因就是作者目前所在位置使用北京时间(UTC+8),去掉8小时的时区偏移后,结果就是1970年1月1日00:00:00 UTC的时间戳。
深入
查看文档
对于这个结果有疑问,于是翻阅了一下Lua官方文档,可以看到os.time的介绍里有这样一行提示
The returned value is a number, whose meaning depends on your system.
(返回值是一个数字,它的含义取决于你的系统)
所以这里其实已经指出了os.time()的返回值会受到系统时区的影响,因此在跨平台使用时需要特别注意时区差异。
查看源码
但是不满足于此,我又翻了一下lua的源码,看到os.date的实现如下:
static int os_time (lua_State *L) {
time_t t;
if (lua_isnoneornil(L, 1)) /* called without args? */
t = time(NULL); /* get current time */
else {
struct tm ts;
luaL_checktype(L, 1, LUA_TTABLE);
lua_settop(L, 1); /* make sure table is at the top */
ts.tm_sec = getfield(L, "sec", 0, 0);
ts.tm_min = getfield(L, "min", 0, 0);
ts.tm_hour = getfield(L, "hour", 12, 0);
ts.tm_mday = getfield(L, "day", -1, 0);
ts.tm_mon = getfield(L, "month", -1, 1);
ts.tm_year = getfield(L, "year", -1, 1900);
ts.tm_isdst = getboolfield(L, "isdst");
t = mktime(&ts); /* 真正获取时间的地方 */
setallfields(L, &ts); /* update fields with normalized values */
}
if (t != (time_t)(l_timet)t || t == (time_t)(-1))
return luaL_error(L,
"time result cannot be represented in this installation");
l_pushtime(L, t);
return 1;
}
可以的看到真正获取时间的代码是
t = mktime(&ts);
而mktime函数是C标准库中的一个函数,它将一个struct tm结构体转换为time_t类型的值,同时考虑了本地时区的影响。
那么为什么lua要用这样一个函数来实现,有没有接口能够避免时区的影响,确保返回的时间戳是基于UTC的呢?在stackoverflow中找到了答案,简而言之目前没有比较简单和通用的方法来避免时区的影响。不过在部分平台可以使用其它函数,例如在windows平台可以使用_mkgmtime,而在linux和BSD可以使用timegm,但是在PS5平台则没有比较合适的函数。
手动计算
考虑到既然计算规则是很明确的,就是计算给定的日期相对于1970年1月1日0点的时间戳,那么理论上我们是可以自己实现计算逻辑的。
正巧我发现luau(Lua语言的一个分支,但它包含了一些改进和优化)实现了上述计算逻辑,在luau中os.time()返回的是UTC时间的时间戳,其计算时间戳代码如下:
static time_t os_timegm(struct tm* timep)
{
// Julian day number calculation
int day = timep->tm_mday;
int month = timep->tm_mon + 1;
int year = timep->tm_year + 1900;
// year adjustment, pretend that it starts in March
int a = timep->tm_mon % 12 < 2 ? 1 : 0;
// also adjust for out-of-range month numbers in input
a -= timep->tm_mon / 12;
int y = year + 4800 - a;
int m = month + (12 * a) - 3;
int julianday = day + ((153 * m + 2) / 5) + (365 * y) + (y / 4) - (y / 100) + (y / 400) - 32045;
const int utcstartasjulianday = 2440588; // Jan 1st 1970 offset in Julian calendar
const int64_t utcstartasjuliansecond = utcstartasjulianday * 86400ll; // same in seconds
// fail the dates before UTC start
if (julianday < utcstartasjulianday)
return time_t(-1);
int64_t daysecond = timep->tm_hour * 3600ll + timep->tm_min * 60ll + timep->tm_sec;
int64_t julianseconds = int64_t(julianday) * 86400ull + daysecond;
if (julianseconds < utcstartasjuliansecond)
return time_t(-1);
int64_t utc = julianseconds - utcstartasjuliansecond;
return time_t(utc);
}