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);
}
posted @ 2024-09-02 21:11  heanrum  阅读(122)  评论(0编辑  收藏  举报