Funções utilitárias
Calcular o deslocamento do fuso local em relação ao UTC.
local Ferramentas = {}
function Ferramentas.deslocamentoLocal()
local agora = os.time()
local utc = os.date("!*t", agora)
return os.difftime(agora, os.time(utc))
end
Exibir um timestamp no formato UTC+0.
function Ferramentas.imprimirUTC0(epoch)
local d = os.date("!*t", epoch)
print(string.format("%04d-%02d-%02d %02d:%02d:%02d UTC+0",
d.year, d.month, d.day, d.hour, d.min, d.sec))
end
Converter uma string ISO 8601 em uma tabela de data, opcionalmente com o deslocamento do fuso.
function Ferramentas.parseISO8601(texto)
local padrao =
"^(%d%d%d%d)%-(%d%d)%-(%d%d)[T ](%d%d):(%d%d):(%d%d)([%+%-]?)(%d?%d?):?(%d?%d?)$"
local ano, mes, dia, hora, min, seg, sinal, htz, mtz =
string.match(texto, padrao)
if not ano then
error("formato inválido: " .. tostring(texto))
end
local data = {
year = tonumber(ano),
month = tonumber(mes),
day = tonumber(dia),
hour = tonumber(hora),
min = tonumber(min),
sec = tonumber(seg),
tzOffset = nil,
}
if htz and htz ~= "" then
local desl = tonumber(htz) * 3600
if mtz and mtz ~= "" then
desl = desl + tonumber(mtz) * 60
end
if sinal == "-" then
desl = -desl
end
data.tzOffset = desl
end
return data
end
Classe ZonaHoraria
A classe armazena o deslocamento do fuso de interesse, o deslocamento da máquina local e as fronteiras do horário de verão.
local ZonaHoraria = {}
ZonaHoraria.__index = ZonaHoraria
function ZonaHoraria:novo()
return setmetatable({
deslocamentoFuso = 0,
deslocamentoLocal = 0,
inicioVerao = 0,
fimVerao = 0,
}, ZonaHoraria)
end
function ZonaHoraria:definirDeslocamentoLocal(v)
self.deslocamentoLocal = v
end
function ZonaHoraria:definirDeslocamentoFuso(v)
self.deslocamentoFuso = v
end
local Interno = {}
function Interno.epochDeStringISO(self, texto)
local data = Ferramentas.parseISO8601(texto)
-- os.time interpreta os campos como horário local; reverte-se essa suposição.
local epoch = os.time(data) + self.deslocamentoLocal
local desl = data.tzOffset
if desl then
epoch = epoch - desl
else
epoch = epoch - self.deslocamentoFuso
end
return epoch
end
function ZonaHoraria:definirVeraoPorTexto(inicio, fim)
self.inicioVerao = Interno.epochDeStringISO(self, inicio)
self.fimVerao = Interno.epochDeStringISO(self, fim)
end
function ZonaHoraria:definirVeraoPorEpoch(inicio, fim)
self.inicioVerao = inicio
self.fimVerao = fim
end
function ZonaHoraria:suportaVerao()
return self.inicioVerao ~= 0 and self.fimVerao ~= 0
end
Convertendo data para timestamp
Base comum que impede que o sistema operacional aplique regras locais de DST.
function Interno.epochBase(self, data)
local copia = {
year = data.year,
month = data.month,
day = data.day,
hour = data.hour,
min = data.min,
sec = data.sec,
isdst = false,
}
return os.time(copia) + self.deslocamentoLocal
end
Escolha entre os candidatos padrão e de verão quando o sinalizador isdst está ausente.
function Interno.escolherEpoch(self, base)
local tsPadrao = base - self.deslocamentoFuso
local tsVerao = base - (self.deslocamentoFuso + 3600)
local padraoEVerao = self:timestampEhVerao(tsPadrao)
local veraoEVerao = self:timestampEhVerao(tsVerao)
if padraoEVerao == veraoEVerao then
return padraoEVerao and tsVerao or tsPadrao
elseif padraoEVerao then
-- hora inexistente na mudança de primavera: usa o instante anterior
return tsVerao
else
-- hora ambígua na mudança de outono: usa o instante posterior
return tsPadrao
end
end
Conversão a partir de uma tabela de data.
function ZonaHoraria:dataParaEpoch(data)
local base = Interno.epochBase(self, data)
local estado = data.isdst
if estado == true then
return base - (self.deslocamentoFuso + 3600)
elseif estado == false then
return base - self.deslocamentoFuso
else
if self:suportaVerao() then
return Interno.escolherEpoch(self, base)
else
return base - self.deslocamentoFuso
end
end
end
Conversão a partir de uma string ISO 8601.
function ZonaHoraria:textoParaEpoch(texto)
local data = Ferramentas.parseISO8601(texto)
local base = Interno.epochBase(self, data)
if data.tzOffset then
return base - data.tzOffset
elseif self:suportaVerao() then
return Interno.escolherEpoch(self, base)
else
return base - self.deslocamentoFuso
end
end
Conversão a partir de componentes individuais.
function ZonaHoraria:componentesParaEpoch(ano, mes, dia, hora, min, seg, ehVerao)
return self:dataParaEpoch({
year = ano, month = mes, day = dia,
hour = hora, min = min, sec = seg,
isdst = ehVerao,
})
end
Verificação de horário de verão
function ZonaHoraria:timestampEhVerao(epoch)
if not self:suportaVerao() then
return false
end
return epoch >= self.inicioVerao and self.fimVerao > epoch
end
function ZonaHoraria:dataEhVerao(data)
return self:timestampEhVerao(self:dataParaEpoch(data))
end
function ZonaHoraria:textoEhVerao(texto)
return self:timestampEhVerao(self:textoParaEpoch(texto))
end
Timestamp para data
function ZonaHoraria:epochParaData(epoch)
local desl = self.deslocamentoFuso
if self:timestampEhVerao(epoch) then
desl = desl + 3600
end
local data = os.date("!*t", epoch + desl)
data.isdst = self:timestampEhVerao(epoch)
return data
end
function Ferramentas.formatarData(data)
local extra = data.isdst and " dst" or ""
return string.format("%04d-%02d-%02d %02d:%02d:%02d%s",
data.year, data.month, data.day,
data.hour, data.min, data.sec, extra)
end
function ZonaHoraria:epochParaTexto(epoch)
local data = self:epochParaData(epoch)
local desl = self.deslocamentoFuso
if data.isdst then
desl = desl + 3600
end
local sinal = desl >= 0 and "+" or "-"
local h = math.floor(math.abs(desl) / 3600)
local m = math.floor(math.abs(desl) % 3600 / 60)
return string.format("%04d-%02d-%02d %02d:%02d:%02d%s%02d:%02d",
data.year, data.month, data.day,
data.hour, data.min, data.sec,
sinal, h, m)
end
Testes
local function assertIgual(esperado, obtido)
if esperado ~= obtido then
error(string.format("esperado %s, obtido %s",
tostring(esperado), tostring(obtido)))
end
end
local fuso = ZonaHoraria:novo()
fuso:definirDeslocamentoLocal(Ferramentas.deslocamentoLocal())
fuso:definirDeslocamentoFuso(-5 * 3600)
fuso:definirVeraoPorTexto(
"2024-03-10T02:00:00-05:00",
"2024-11-03T02:00:00-04:00"
)
local inicio = 1710054000
local fim = 1730613600
assertIgual(0, fuso.inicioVerao - inicio)
assertIgual(0, fuso.fimVerao - fim)
assertIgual(true, fuso:suportaVerao())
-- texto -> epoch
do
local t1 = fuso:textoParaEpoch("2024-03-10 01:00:00")
local t2 = fuso:textoParaEpoch("2024-03-10 02:00:00")
local t3 = fuso:textoParaEpoch("2024-03-10 03:00:00")
assertIgual(-3600, t1 - inicio)
assertIgual(-3600, t2 - inicio)
assertIgual(0, t3 - inicio)
local t4 = fuso:textoParaEpoch("2024-11-03 00:00:00")
local t5 = fuso:textoParaEpoch("2024-11-03 01:00:00")
local t6 = fuso:textoParaEpoch("2024-11-03 02:00:00")
assertIgual(-7200, t4 - fim)
assertIgual(0, t5 - fim)
assertIgual(3600, t6 - fim)
end
-- data -> epoch
do
local t1 = fuso:dataParaEpoch({year=2024, month=3, day=10, hour=1, min=0, sec=0})
local t2 = fuso:dataParaEpoch({year=2024, month=3, day=10, hour=2, min=0, sec=0})
local t3 = fuso:dataParaEpoch({year=2024, month=3, day=10, hour=2, min=0, sec=0, isdst=false})
local t4 = fuso:dataParaEpoch({year=2024, month=3, day=10, hour=2, min=0, sec=0, isdst=true})
local t5 = fuso:dataParaEpoch({year=2024, month=3, day=10, hour=3, min=0, sec=0})
assertIgual(-3600, t1 - inicio)
assertIgual(-3600, t2 - inicio)
assertIgual(0, t3 - inicio)
assertIgual(-3600, t4 - inicio)
assertIgual(0, t5 - inicio)
end
-- verificação de DST
do
assertIgual(false, fuso:textoEhVerao("2024-03-10 01:00:00"))
assertIgual(false, fuso:textoEhVerao("2024-03-10 02:00:00"))
assertIgual(true, fuso:textoEhVerao("2024-03-10 03:00:00"))
assertIgual(true, fuso:textoEhVerao("2024-11-03 00:00:00"))
assertIgual(false, fuso:textoEhVerao("2024-11-03 01:00:00"))
assertIgual(false, fuso:textoEhVerao("2024-11-03 02:00:00"))
end
-- epoch -> data
do
local d1 = fuso:epochParaData(inicio - 3600)
local d2 = fuso:epochParaData(inicio)
local d3 = fuso:epochParaData(inicio + 3600)
assertIgual("2024-03-10 01:00:00", Ferramentas.formatarData(d1))
assertIgual("2024-03-10 03:00:00 dst", Ferramentas.formatarData(d2))
assertIgual("2024-03-10 04:00:00 dst", Ferramentas.formatarData(d3))
local d4 = fuso:epochParaData(fim - 3600)
local d5 = fuso:epochParaData(fim - 1)
local d6 = fuso:epochParaData(fim)
local d7 = fuso:epochParaData(fim + 3600)
assertIgual("2024-11-03 01:00:00 dst", Ferramentas.formatarData(d4))
assertIgual("2024-11-03 01:59:59 dst", Ferramentas.formatarData(d5))
assertIgual("2024-11-03 01:00:00", Ferramentas.formatarData(d6))
assertIgual("2024-11-03 02:00:00", Ferramentas.formatarData(d7))
end