Confusões com datas
É comum ao lidar com sistemas legados que nos deparemos com todo tipo de confusão com datas. Às vezes o sistema começou pequeno e não havia preocupação com fuso horário. As informações de data vão sendo armazenadas como se tudo fosse no horário local, mas se o negócio se expandir?
Alguns exemplos de confusão com datas:
- Um usuário em outro país pode informar uma data-hora que é local pra ele, não vai ser para os usuários “originais”.
- Um prazo estipulado apenas por data pode finalizar horas antes ou depois do esperado, dependendo de fuso do servidor e do cliente. Como também de como a informações são tratadas na entrada e na saída desses dados.
- Um campo de data pode ser preenchido com um dia a mais ou a menos por conta da diferença de poucas horas.
Em alguns casos, basta adotar como regra que, todas as datas estão e/ou serão armazenadas no fuso horário do servidor e devidamente recalculadas quando necessário.
A modelagem é outra coisa que pode complicar muito o trabalho com datas.
Já me deparei com projetos que armazenavam as datas como string, que separavam um campo para data e outro para hora.
Toda linguagem tem um tipo nativo para manipular datas.
Aqui eu focarei nas minhas experiências com Go, mas os exemplos devem se aplicar a outras linguagens.
Para um sistema que está começando, sugiro que se adote o fuso UTC ou que as datas sempre sejam representadas acompanhadas do devido fuso de origem.
O valor 2024-11-26 15:00:00 do meu horário local pode ser representado como 2024-11-26T18:00:00Z ou 2024-11-26T15:00:00-03:00.
Das duas formas, a conversão para um tipo nativo da sua linguagem deve ter o resultado esperado.
Num banco como o postgresql, grosseiramente falando, as datas sempre são convertidas e armazenadas internamente como números (unix timestamp) e depois serão novamente convertidas para o formato que o “client” desejar. Então, para o banco de dados, internamente fica fácil comparar e ordenar esses dados.
Vejam estes exemplos. O resultado dessas comparações é sempre true:
SELECT '2024-11-26 15:00:00'::timestamptz = '2024-11-26T18:00:00Z'::timestamptz;
SELECT '2024-11-26 00:00:00Z'::timestamptz = '2024-11-25T21:00:00-03:00'::timestamptz;
SELECT '2024-11-27Z'::timestamptz < '2024-11-26T21:00:01'::timestamptz;
Em Go, usamos a função time.Parse para converter strings para o tipo time.Time.
Algo como:
t, _ := time.Parse(time.DateTime, "2024-11-26 15:00:00")
fmt.Println(t)
Terá a saída:
2024-11-26 15:00:00 +0000 UTC
Mas se a string representar uma data-hora no fuso local, pode-se fazer:
t, _ = time.ParseInLocation(time.DateTime, "2024-11-26 15:00:00", time.Local)
fmt.Println(t)
2024-11-26 15:00:00 -0300 -03
Neste último exemplo temos uma “pegadinha”.
A variável time.Local será iniciada pelo runtime da linguagem usando o valor setado na variável de ambiente TZ (Ex.: America/Maceio)
Pra quem usa containers, aproveito para avisar que não basta setar a variável TZ.
O runtime do Go busca as informações do fuso horário em arquivos de definição de timezone do sistema.
Então, em imagens minimalistas como scratch, busybox ou mesmo alpine, provavelmente vocês terão que copiar os devidos arquivos em /usr/share/zoneinfo ou instalar o pacote tzdata.
Situações em aplicações web
Em HTML, temos a opção de usar a tag input com os tipos date e datetime-local (o tipo está obsoleto).datetime
| tag | exemplo |
|---|---|
type="date" | |
type="datetime-local" | |
type="datetime-local" step="1" | |
type="datetime-local" step="3600" |
As implementações são diferentes dependendo do browser.
A data pode ser mostrada no seu formato local, mas será enviada no padrão 2024-11-26 e 2024-11026T15:00, respectivamente.
Em ambos os casos, não há referência ao timezone.
Um curiosidade sobre o datetime-local é que você pode definir o tamanho dos “passos”.
O padrão é step="60". Se você quiser deixar o usuário escolher os segundos, basta definir o step para 1.
Seguindo a mesma lógica, se quiser que o usuário só consiga marcar horários “redondos”, basta definir step="3600".
Como é de se esperar, para o tipo date, o “step” é contado em dias.
Outra sugestão é usar os valores min e max para limitar o intervalo de datas permitidas.
No Javascript, a confusão é pior ainda!
const dt = new Date('2024-11-26 15:00:00')
console.log(dt)
Saída no meu browser:
Tue Nov 26 2024 15:00:00 GMT-0300 (Brasilia Standard Time)
Saída no terminal (via NodeJS):
2024-11-26T18:00:00.000Z
A expressão new Date(), retorna um objeto do tipo Date com a hora atual.
Já Date.now() retorna o unix timestamp atual em milisegundos.
Podemos usar este valor para criar um novo objeto Date com a data de ontem:
const dt = new Date(Date.now() - 24 * 60 * 60 * 1000)
console.log(dt)
Mas o que eu acho que deve causar mais confusão é que os meses começam a contar do 0 (diferente do dia e do ano).
Então new Date(2025, 1, 1), será o primeiro dia de fevereiro de 2025! Como não podia deixar de ficar pior,
podemos usar valores maiores: new Date(2025, 13, 60) será o primeiro de abril de 2026!! Isso até ajuda em algumas
operações com datas, mas é bem confuso até você se acostumar!
Conclusão
Pois é, um assunto que parece fácil pode causar muita confusão. Vejam que nem abordei situaçṍes críticas ou que exigem mais precisão.