Lidando com datas em JavaScript (ou "criei uma data, mas mostra um dia a menos")
Um erro "clássico" que ocorre com JavaScript, ao criar uma data específica e imprimi-la, é ele mostrar um valor diferente do que você esperava:
// 1 de março de 2023
const data = new Date('2023-03-01');
// ao imprimir, mostra 28 de fevereiro
console.log(data.toLocaleString('pt-BR')); // 28/02/2023, 21:00:00 ????
O que aconteceu? Bem, vamos por partes...
O que é o Date
do JavaScript?
O Date
, apesar do nome, não é exatamente uma data. Pelo menos não no sentido de representar uma única combinação de dia, mês, ano, hora, minuto e segundo.
Na verdade, segundo a especificação da linguagem, o único valor que um Date
possui é um número que corresponde ao Unix timestamp. Mais precisamente, esse número é a quantidade de milissegundos que se passaram desde o Unix Epoch (que seria o "instante zero").
O "instante zero", por sua vez, corresponde a
1970-01-01T00:00Z
- 1 de janeiro de 1970 à meia-noite, em UTC.
Portanto, o Date
representa um instante único, um ponto na linha do tempo. Pense no "agora": neste exato momento, que dia é hoje e que horas são? Em cada parte do mundo, a resposta será diferente (em algumas partes do mundo, é 1 de março, em outras pode ser dia 2 de março ou 28 de fevereiro, e o horário também pode ser diferente). Apesar dos valores numéricos de data e hora serem diferentes, o instante (o valor do timestamp) é o mesmo para todos. E o Date
só possui o valor do timestamp.
O que acontece é que quando você imprime a data (via alert
ou console.log
, por exemplo), ou obtém informações dela (seja via os getters ou toString()
), ou a converte para algum formato (como acontece com toLocaleString()
), este timestamp é convertido para alguma data e hora específica, usando para isso o fuso horário (timezone) que está configurado no ambiente onde o código está rodando (browser, Node, Deno, etc). Ou seja, o Date
usa a informação do timezone para converter o timestamp para os valores numéricos de data e hora.
Constrir um Date
a partir de uma string
Outro ponto é que, quando passamos uma string ao construtor de Date
, como foi feito no código acima, ele usa as regras do método Date.parse
. E segundo essas regras, uma string no formato "AAAA-MM-DD" (que é definido pela norma ISO 8601), ou seja, somente com a data e sem horário, é interpretado como meia-noite em UTC.
Portanto, no código acima, a data corresponde a 1 de março, à meia-noite em UTC. Mas meu browser está configurado com o Horário de Brasília, e por isso toLocaleString
mostrou os valores de data e hora correspondentes a este timezone. E como o Horário de Brasília está 3 horas antes de UTC, ele mostra a data "errada".
Lembrando que se seu ambiente está usando outro timezone, os resultados serão diferentes do meu.
E só pra confundir ainda mais, existem métodos em Date
que trabalham com UTC, como toISOString()
. Veja a diferença:
const data = new Date('2023-03-01');
// toLocaleString usa o timezone do browser (no meu caso, é o Horário de Brasília), por isso mostra 28 de fevereiro
console.log(data.toLocaleString('pt-BR')); // 28/02/2023, 21:00:00
// toISOString usa sempre UTC, então mostra 1 de março à meia-noite (o "Z" no final indica que é UTC)
console.log(data.toISOString()); // 2023-03-01T00:00:00.000Z
Como resolver?
Existem várias soluções diferentes.
Uma é adicionando o horário na string. Parece gambiarra, mas a especificação da linguagem define que se a string contém o horário, aí ele usa o timezone do browser/ambiente em vez de UTC:
date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time
Não me pergunte porque decidiram assim. Como disse uma vez um professor: "Eu não criei as regras, eu só as ensino". Então ficaria assim:
// string tem horário, então agora usa o timezone do browser
const data = new Date('2023-03-01T00:00');
console.log(data.toLocaleString('pt-BR')); // 01/03/2023, 00:00:00
// lembrando que o Horário de Brasília está 3 horas antes de UTC
// por isso se usar métodos que trabalham com UTC, ficará 3 horas "na frente"
console.log(data.toISOString()); // 2023-03-01T03:00:00.000Z
Ou, se eu já sei que a data criada considera UTC, então basta usar UTC na hora de formatá-la:
// string sem horário, então usa UTC
const data = new Date('2023-03-01');
// indico que quero usar UTC
console.log(data.toLocaleString('pt-BR', { timeZone: 'UTC' })); // 01/03/2023, 00:00:00
Ou então você pode extrair os valores numéricos da string e passá-los separadamente para o construtor, pois aí ele também considera que o horário é meia-noite no timezone do browser. Um detalhe chato é que ao usar valores numéricos, o mês é indexado em zero (janeiro é zero, fevereiro é 1, etc), então tem que lembrar de subtrair 1
:
const string = '2023-03-01';
// extrai os valores numéricos
const [ano, mes, dia] = string.split('-').map(v => parseInt(v));
// assim, ele considera meia-noite no timezone do browser
const data = new Date(ano, mes - 1, dia); // lembrando que agora janeiro é zero, fevereiro é 1, etc
console.log(data.toLocaleString('pt-BR')); // 01/03/2023, 00:00:00
Claro, a "melhor" solução depende do que você precisa. Lembrando que há diferenças entre usar meia-noite em UTC ou no timezone do browser, já que correspondem a instantes diferentes (a menos, é claro, que o timezone seja igual a UTC, como ocorre na Inglaterra quando não está em horário de verão). Ou seja, o valor do timestamp não será o mesmo:
// meia-noite no timezone do browser (no meu caso, Horário de Brasília)
console.log(new Date('2023-03-01T00:00').getTime()); // 1677639600000
// meia-noite em UTC
console.log(new Date('2023-03-01').getTime()); // 1677628800000
Dependendo do que você vai fazer com a data, pode ou não fazer diferença. Por isso não existe o método "correto" para todas as situações. O importante é saber como cada alternativa funciona, e ver qual se encaixa melhor no seu caso.
Baseado neste post do meu blog.