lua中处理时区和夏令时

一些工具函数

获取本地时区偏差

---@return integer 当前时区与UTC时间的偏移秒数
function TimeUtil.GetLocalTimeZoneOffset()
    local timestamp = os.time()
    local date = os.date("!*t", timestamp)
    local tzOffset = os.difftime(timestamp, os.time(date))
    return tzOffset
end

 

---用0时区时间字符串打印
function TimeUtilT.PrintTimestampUTC0(timestamp)
    local date = os.date("!*t", timestamp)
    local timeStr = string.format("%04d-%02d-%02d %02d:%02d:%02d UTC+0", date.year, date.month, date.day, date.hour, date.min, date.sec)
    print(timeStr)
end

 

解析时间字符串

---@class DateParsed
---@field year integer
---@field month integer
---@field day integer
---@field hour integer
---@field min integer
---@field sec integer
---@field tzOffset integer|nil 时区偏移, 以秒为单位, 例如: -5*3600表示UTC-5, +8*3600表示UTC+8, nil表示没有时区信息

---@param timeStr string ISO8601格式的时间字符串, 例如: "2024-10-03T01:00:00-05:00", "2024-10-03 01:00:00-05:00", "2024-10-03 01:00:00"
---@return DateParsed date
function TimeUtil.TimeStrToDate(timeStr)
    local pattern = "^(%d+)-(%d+)-(%d+)[T ](%d+):(%d+):(%d+)([%+%-]?)(%d?%d?):?(%d?%d?)$"
    local index1, index2, year, month, day, h, m, s, tzSign, tzHour, tzMin = string.find(timeStr, pattern)
    local date = { year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0, tzOffset = 0 }
    date.year = tonumber(year)
    date.month = tonumber(month)
    date.day = tonumber(day)
    date.hour = tonumber(h)
    date.min = tonumber(m)
    date.sec = tonumber(s)
    local tzOffset = nil
    if tzHour and "" ~= tzHour then
        tzOffset = tonumber(tzHour) * 3600
    end
    if tzMin and "" ~= tzMin then
        if nil == tzOffset then
            tzOffset = 0
        end
        tzOffset = tzOffset + tonumber(tzMin) * 60
    end
    if tzOffset and "-" == tzSign then
        tzOffset = -tzOffset
    end
    date.tzOffset = tzOffset
    return date
end

 


 TimeZone类

local Internal = {}

function TimeZone:ctor()
    self.m_TimeZoneOffset = 0
    self.m_LocalTimeZoneOffset = 0
    self.m_DstBeginTimestamp = 0
    self.m_DstEndTimestamp = 0
end

---@param self TimeZone
function Internal.DstTimeStrToTimestamp(self, timeStr)
    local date = TimeUtil.TimeStrToDate(timeStr)
    local timestamp = os.time(date) --内部会减掉本地时区偏差, 比如: UTC+8会减掉 8*3600
    timestamp = timestamp + self.m_LocalTimeZoneOffset --因为timeStr不是本地时区的时间, 把上面减掉的加回来

    --像os.time做的那样, 减掉timeStr所在时区的偏差
    local tzOffset = date.tzOffset
    if tzOffset then
        timestamp = timestamp - tzOffset -- 比如: -04:00就是-(-4*3600)
    else
        timestamp = timestamp - self.m_TimeZoneOffset -- 比如: UTC-5就是-(-5*3600)
    end
    return timestamp
end

function TimeZone:SetDstByTimeStr(dstBeginTimeStr, dstEndTimeStr)
    self.m_DstBeginTimestamp = Internal.DstTimeStrToTimestamp(self, dstBeginTimeStr)
    self.m_DstEndTimestamp = Internal.DstTimeStrToTimestamp(self, dstEndTimeStr)
end

function TimeZone:SetDstTimestamp(dstBeginTimestamp, dstEndTimestamp)
    self.m_DstBeginTimestamp = dstBeginTimestamp
    self.m_DstEndTimestamp = dstEndTimestamp
end

---该时区是否有夏令时
function TimeZone:IsSupportDst()
    local b = (nil ~= self.m_DstBeginTimestamp) and (nil ~= self.m_DstEndTimestamp)
    return b
end

 

1) 时间转时间戳

a) 公共代码

---获取没有减掉本地时区的timestamp
function Internal.GetFixedTimestamp(self, date)
    date.isdst = false --不让os.time内部处理, 自己手动处理
    local timestamp = os.time(date) --内部会减掉本地时区, 比如: UTC+8就是减8*3600
    local timestampWithTzOffset = timestamp + self.m_LocalTimeZoneOffset --因为timeStr不是本地时区的时间, 把上面减掉的加回来
    return timestampWithTzOffset
end

---在(日期时间_当前时区)和(日期时间_夏令时区)之间选一个来计算间戳, 主要目的是确保在本地时区和夏令时区显示的时间部分保持一致。
---比如: 对于美国东部时区(UTC-5)每天8点上班
---1) 2024-02-10 08:00:00, 这肯定是一个非夏令时间, 会用 2024-02-10 08:00:00(EST,UTC-5)对应的时间戳
---2) 2024-05-10 08:00:00, 这肯定是一个夏令时间, 会用 2024-05-10 08:00:00(EDT,UTC-4)对应的时间戳
---@param self TimeZone
---@param timestampWithTzOffset integer 带有当前时区偏差的时间戳
function Internal.JudgeTimestamp(self, timestampWithTzOffset)
    local tzOffset = self.m_TimeZoneOffset
    local dstTzOffset = self.m_TimeZoneOffset + 3600 --当前时区转为夏令时区, 比如: (EST, UTC-5) -> (EDT, UTC-4)就是加1小时
    local timestamp_1 = timestampWithTzOffset - tzOffset --当前时区对应的时间戳
    local timestamp_2 = timestampWithTzOffset - dstTzOffset --夏令时区对应的时间戳

    local isdst_1 = self:TimestampIsDst(timestamp_1)
    local isdst_2 = self:TimestampIsDst(timestamp_2)
    if isdst_1 == isdst_2 then
        if isdst_1 then
            return timestamp_2 --都是夏令时, 使用 日期时间_夏令时区 对应的时间戳
        else
            return timestamp_1 --都不是夏令时, 使用 日期时间_当前时区 对应的时间戳
        end
    else --一个是夏令时, 一个不是夏令时
        if isdst_1 then
            return timestamp_2 --返回 日期时间_夏令时区 对应的时间戳, 即: 夏令时开始前的时间, 比如: 2024-03-10 02:30:00(EDT,UTC-4), 而不是02:30:00(EST,UTC-5)
        else
            return timestamp_1 --返回 日期时间_当前时区 对应的时间戳, 即: 夏令时结束后的时间, 比如: 2024-11-03 01:30:00(EST,UTC-5), 而不是01:30:00(EDT,UTC-4)
        end
    end
end

b) 时间转时间戳

---@return integer timestamp
function TimeZone:DateToTimestamp(date)
    local isdst = date.isdst

    local timestampWithTzOffset = Internal.GetFixedTimestamp(self, date)

    local timestamp = 0
    --像os.time做的那样, 减掉对应的时区偏差
    if true == isdst then --表示date是一个夏令时时间
        local tzOffset = self.m_TimeZoneOffset + 3600 --当前时区转为夏令时, 比如: (EST, UTC-5) -> (EDT, UTC-4)就是加1小时
        timestamp = timestampWithTzOffset - tzOffset --比如: (EDT, UTC-4)就是-(-4*3600)
    elseif false == isdst then --表示date是一个非夏令时时间
        timestamp = timestampWithTzOffset - self.m_TimeZoneOffset --比如: (EST, UTC-5)就是-(-5*3600)
    elseif nil == isdst  then
        if self:IsSupportDst() then
            timestamp = Internal.JudgeTimestamp(self, timestampWithTzOffset)
        else
            timestamp = timestampWithTzOffset - self.m_TimeZoneOffset --比如: (EST, UTC-5)就是-(-5*3600)
        end
    end
    return timestamp
end

function TimeZone:TimeStrToTimestamp(timeStr)
    local date = TimeUtil.TimeStrToDate(timeStr)
    local timestampWithTzOffset = Internal.GetFixedTimestamp(self, date)
    
    local timestamp = 0
    --像os.time做的那样, 减掉timeStr所在时区的偏差
    local tzOffset = date.tzOffset
    if tzOffset then
        timestamp = timestampWithTzOffset - tzOffset -- 比如: -04:00就是-(-4*3600)
    elseif self:IsSupportDst() then
        timestamp = Internal.JudgeTimestamp(self, timestampWithTzOffset)
    else
        timestamp = timestampWithTzOffset - self.m_TimeZoneOffset -- 比如: UTC-5就是-(-5*3600)
    end
    return timestamp
end

---@param isdst boolean? 为nil时, 表示动态判断并在 日期时间_当前时区 和 日期时间_夏令时区 中选一个来计算时间戳
function TimeZone:TimeToTimestamp(year, month, day, hour, min, sec, isdst)
    local date = { year = year, month = month, day = day, hour = hour, min = min, sec = sec, isdst = isdst }
    local timestamp = self:DateToTimestamp(date)
    return timestamp
end

 

2) 夏令时判断

function TimeZone:TimestampIsDst(timestamp)
    if self:IsSupportDst() then
        if timestamp < self.m_DstBeginTimestamp then
            return false
        end
        if timestamp >= self.m_DstEndTimestamp then
            return false
        end
        return true
    end
    return false
end

function TimeZone:DateIsDst(date)
    local timestamp = self:DateToTimestamp(date)
    local b = self:TimestampIsDst(timestamp)
    return b
end

function TimeZone:TimeStrIsDst(timeStr)
    local timestamp = self:TimeStrToTimestamp(timeStr)
    local b = self:TimestampIsDst(timestamp)
    return b
end

 

3) 时间戳转时间
function TimeZone:TimestampToDate(timestamp)
    local tzOffset = self.m_TimeZoneOffset
    if self:TimestampIsDst(timestamp) then
        tzOffset = tzOffset + 3600 --当前时区转为夏令时, 比如: (EST, UTC-5) -> (EDT, UTC-4)就是加1小时
        local timestampWithTzOffset = timestamp + tzOffset
        local date = os.date("!*t", timestampWithTzOffset)
        date.isdst = true
        return date
    else
        local timestampWithTzOffset = timestamp + tzOffset
        local date = os.date("!*t", timestampWithTzOffset)
        return date
    end
end

function TimeZone:TimestampToTimeStr(timestamp)
    local date = self:TimestampToDate(timestamp)
    local tzHour = math.floor(self.m_TimeZoneOffset / 3600)
    local tzMin = self.m_TimeZoneOffset - 3600 * tzHour
    local timeStr = ""
    if tzHour >= 0 then
        timeStr = string.format("%04d-%02d-%02d %02d:%02d:%02d%+02d:%02d", date.year, date.month, date.day, date.hour, date.min, date.sec, tzHour, tzMin)
    else
        timeStr = string.format("%04d-%02d-%02d %02d:%02d:%02d%-02d:%02d", date.year, date.month, date.day, date.hour, date.min, date.sec, -tzHour, -tzMin)
    end
    return timeStr
end

 


 TimeZone测试用例

---@type TimeZone
local m_TimeZone = require("Time.TimeZone").new()

m_TimeZone:SetLocalTimeZoneOffset(TimeUtil.GetLocalTimeZoneOffset())
m_TimeZone:SetTimeZoneOffset(-5 * 3600) --美国东部时区(UTC-5)
m_TimeZone:SetDstByTimeStr("2024-03-10T02:00:00-05:00", "2024-11-03T02:00:00-04:00") --夏令时开始和结束时间

AssertEqual(0, m_TimeZone:GetDstBeginTimestamp() - 1710054000)
AssertEqual(0, m_TimeZone:GetDstEndTimestamp() - 1730613600)

AssertEqual(true, m_TimeZone:IsSupportDst())
AssertEqual(1, 1730613600 - m_TimeZone:TimeStrToTimestamp("2024-11-03 01:59:59-04:00"))

local function Test_TimeStrToTimestamp()
    local t1 = m_TimeZone:TimeStrToTimestamp("2024-03-10 01:00:00") --肯定不是夏令时, 开始前。用 日期时间_当前时区 算时间戳
    local t2 = m_TimeZone:TimeStrToTimestamp("2024-03-10 02:00:00") --可能是夏令时, 用不是夏令时的 日期时间_夏令时区 算时间戳
    local t3 = m_TimeZone:TimeStrToTimestamp("2024-03-10 03:00:00") --肯定是夏令时, 开始后。用 日期时间_夏令时区 算时间戳
    AssertEqual(-3600, t1 - 1710054000)
    AssertEqual(-3600, t2 - 1710054000)
    AssertEqual(0, t3 - 1710054000)

    local t4 = m_TimeZone:TimeStrToTimestamp("2024-11-03 00:00:00") --肯定是夏令时,结束前。用 日期时间_夏令时区 算时间戳
    local t5 = m_TimeZone:TimeStrToTimestamp("2024-11-03 01:00:00") --可能是夏令时。用不是夏令时的 日期时间_夏令时区 算时间戳
    local t6 = m_TimeZone:TimeStrToTimestamp("2024-11-03 02:00:00") --肯定不是夏令时, 结束后。用 日期时间_当前时区 算时间戳
    AssertEqual(-7200, t4 - 1730613600)
    AssertEqual(0, t5 - 1730613600)
    AssertEqual(3600, t6 - 1730613600)
end
Test_TimeStrToTimestamp()

 

local function Test_DateToTimestamp()
    local t1 = m_TimeZone:DateToTimestamp({ year = 2024, month = 3, day = 10, hour = 1, min = 0, sec = 0 })
    local t2 = m_TimeZone:DateToTimestamp({ year = 2024, month = 3, day = 10, hour = 2, min = 0, sec = 0 })
    local t3 = m_TimeZone:DateToTimestamp({ year = 2024, month = 3, day = 10, hour = 2, min = 0, sec = 0, isdst = false })
    local t4 = m_TimeZone:DateToTimestamp({ year = 2024, month = 3, day = 10, hour = 2, min = 0, sec = 0, isdst = true })
    local t5 = m_TimeZone:DateToTimestamp({ year = 2024, month = 3, day = 10, hour = 3, min = 0, sec = 0 })
    AssertEqual(-3600, t1 - 1710054000)
    AssertEqual(-3600, t2 - 1710054000)
    AssertEqual(0, t3 - 1710054000)
    AssertEqual(-3600, t4 - 1710054000)
    AssertEqual(0, t5 - 1710054000)

    local t10 = m_TimeZone:DateToTimestamp({ year = 2024, month = 11, day = 3, hour = 0, min = 0, sec = 0 })
    local t11 = m_TimeZone:DateToTimestamp({ year = 2024, month = 11, day = 3, hour = 1, min = 0, sec = 0 })
    local t12 = m_TimeZone:DateToTimestamp({ year = 2024, month = 11, day = 3, hour = 1, min = 0, sec = 0, isdst = false })
    local t13 = m_TimeZone:DateToTimestamp({ year = 2024, month = 11, day = 3, hour = 1, min = 0, sec = 0, isdst = true })
    local t14 = m_TimeZone:DateToTimestamp({ year = 2024, month = 11, day = 3, hour = 2, min = 0, sec = 0 })
    AssertEqual(7200, 1730613600 - t10)
    AssertEqual(0, 1730613600 - t11)
    AssertEqual(0, 1730613600 - t12)
    AssertEqual(3600, 1730613600 - t13)
    AssertEqual(-3600, 1730613600 - t14)
end
Test_DateToTimestamp()

 

local function Test_IsDst()
    local b1 = m_TimeZone:TimeStrIsDst("2024-03-10 01:00:00") --肯定不是夏令时, 夏令时开始前
    local b2 = m_TimeZone:TimeStrIsDst("2024-03-10 02:00:00") --不是夏令时
    local b3 = m_TimeZone:TimeStrIsDst("2024-03-10 03:00:00") --肯定是夏令时, 夏令时开始后
    AssertEqual(false, b1)
    AssertEqual(false, b2)
    AssertEqual(true, b3)

    local b4 = m_TimeZone:TimeStrIsDst("2024-11-03 00:00:00") --肯定是夏令时,夏令时结束前
    local b5 = m_TimeZone:TimeStrIsDst("2024-11-03 01:00:00") --不是夏令时
    local b6 = m_TimeZone:TimeStrIsDst("2024-11-03 02:00:00") --肯定不是夏令时, 夏令时结束后
    AssertEqual(true, b4)
    AssertEqual(false, b5)
    AssertEqual(false, b6)
end
Test_IsDst()

 

local function Test_TimestampToDate()
    local date1 = m_TimeZone:TimestampToDate(1710054000 - 3600)
    local date2 = m_TimeZone:TimestampToDate(1710054000)
    local date3 = m_TimeZone:TimestampToDate(1710054000 + 3600)
    AssertEqual("2024-03-10 01:00:00", TimeUtil.FormatDate(date1))
    AssertEqual("2024-03-10 03:00:00 dst", TimeUtil.FormatDate(date2))
    AssertEqual("2024-03-10 04:00:00 dst", TimeUtil.FormatDate(date3))

    local date3 = m_TimeZone:TimestampToDate(1730613600 - 3600)
    local date4 = m_TimeZone:TimestampToDate(1730613600 - 1)
    local date5 = m_TimeZone:TimestampToDate(1730613600)
    local date6 = m_TimeZone:TimestampToDate(1730613600 + 3600)
    AssertEqual("2024-11-03 01:00:00 dst", TimeUtil.FormatDate(date3))
    AssertEqual("2024-11-03 01:59:59 dst", TimeUtil.FormatDate(date4))
    AssertEqual("2024-11-03 01:00:00", TimeUtil.FormatDate(date5))
    AssertEqual("2024-11-03 02:00:00", TimeUtil.FormatDate(date6))
end
Test_TimestampToDate()

 

posted @ 2025-08-30 15:32  yanghui01  阅读(16)  评论(0)    收藏  举报