Aritmética de datas e o horário de verão (ou "Um dia pode não ter 24 horas")
Este post é mais um da série sobre datas que estou escrevendo. Os anteriores são:
Hoje vamos ver que um dia nem sempre tem 24 horas. E nem estou falando do fato de que a rotação da Terra está cada vez mais lenta, e sim de algo que pode afetar diretamente os cálculos de data de um sistema, se não tomarmos os devidos cuidados: fusos horários e o famigerado horário de verão.
Neste exato momento, a data e hora atual, em cada lugar do mundo, pode ser diferente. Enquanto em São Paulo são duas da tarde de um domingo, no Japão já são duas da manhã de segunda-feira. Mas dependendo da época do ano, pode ser que duas da tarde em São Paulo seja equivalente a uma (e não duas) da manhã no Japão. Tudo por causa do horário de verão.
Neste texto vamos considerar o Horário de Brasília, mas o problema ocorrerá em qualquer lugar que adote o horário de verão, ou cujo fuso horário tenha sido mudado por qualquer motivo que seja. Então para entender porque somar 1 dia nem sempre é o mesmo que somar 24 horas, primeiro precisamos entender como funciona o horário de verão.
De forma resumida, o horário de verão consiste em atrasar ou adiantar o relógio uma determinada quantidade de tempo (na maioria dos lugares, essa quantidade é uma hora), em determinada data e horário. No caso do Horário de Brasília, por exemplo, em 14 de outubro de 2017, à meia-noite, os relógios foram adiantados em uma hora. Isso quer dizer que na prática, o relógio pulou de 23:59:59.999 do dia 14 direto para 01:00 do dia 15. Todos os minutos entre 00:00 e 00:59 não existiram no dia 14, para este fuso horário. Ou seja, o dia 15 começou na verdade às 01:00, e portanto neste fuso horário, este dia teve 23 horas.
Isso é chamado de DST Gap: "DST" é a sigla para Daylight Saving Time (o nome em inglês para Horário de Verão, que muitos também chamam de "Summer Time") e gap significa "vão", ou seja, há um "vazio" ali porque uma hora foi "pulada".
Isso é melhor ilustrado pela figura abaixo:
Repare que a "mágica de pular uma hora" é possível porque o que acontece na verdade é uma mudança de offset (a diferença com relação a UTC). Antes do DST Gap, o offset é -03:00
(3 horas antes de UTC), e quando há a virada de 23:59 para 00:00, o offset muda para -02:00
(duas horas antes de UTC). É essa mudança de offset que torna possível adiantar o relógio do ponto de vista local, sem quebrar a continuidade dos instantes referentes a antes e depois da mudança. Repare na figura acima que os instantes em UTC permanecem sendo contínuos.
Já quando o horário de verão termina, acontece algo tão ou mais estranho. Ainda usando como exemplo o Horário de Brasília: quando acaba o horário de verão, o relógio é atrasado em uma hora. Em 2017, por exemplo, o horário de verão acabou no dia 19 de fevereiro: à meia-noite, os relógios foram atrasados em uma hora e voltaram para 23:00 do dia 18.
Ou seja, os relógios pularam de 23:59:59.999 do dia 18 para 23:00 do dia 18. Isso quer dizer que todos os minutos entre 23:00 e 23:59 ocorreram duas vezes: uma horário de verão e outra no horário "normal". Ou seja, neste fuso horário, o dia 18 durou 25 horas.
Isso é chamado de DST Overlap: já vimos acima que "DST" é a sigla em inglês para o horário de verão, e overlap é "sobreposição", para indicar que um mesmo intervalo de tempo ocorreu duas vezes.
Para entender melhor, vejamos a figura abaixo:
O que acontece é que o offset é restaurado para o valor "normal": deixa de ser -02:00
e volta a ser -03:00
. Mas mais uma vez, repare que os instantes em UTC são contínuos, ou seja, apenas o horário local foi alterado graças à mudança de offset. Mas a continuidade da linha do tempo permaneceu intacta.
E como isso afeta a aritmética de datas?
Depende muito do que vc precisa fazer, e de como cada linguagem trata cada caso. Até porque não há uma norma oficial sobre como a aritmética de datas deve funcionar (como há na matemática, por exemplo), o que temos são decisões que cada linguagem ou biblioteca faz ao tratar diversos casos diferentes.
Mas de forma geral, quando somamos um dia a uma data, o esperado (o "senso comum", entre muitas aspas) é que o resultado seja o mesmo horário do dia seguinte. E na grande maioria dos casos, isso será exatamente o mesmo que somar 24 horas. Mas graças ao horário de verão, isso nem sempre é verdade.
Vamos fazer um teste com JavaScript, usando uma data qualquer. Lembrando que os testes abaixo foram feitos em uma máquina cujo fuso horário configurado é o Horário de Brasília (se configurar em fusos diferentes, os resultados não serão os mesmos).
Enfim, para uma mesma data, somo um dia, e depois somo ao timestamp a quantidade de milissegundos equivalente a 24 horas:
var millisPerHour = 3600 * 1000;
var data = new Date(2017, 4, 1, 10, 0);
var antes = data.getTime();
console.log(' antes:', data.toString());
// somando 1 dia
data.setDate(data.getDate() + 1);
var depois = data.getTime();
console.log('depois:', data.toString());
console.log('diferença:', (depois - antes) / millisPerHour);
data = new Date(2017, 4, 1, 10, 0);
antes = data.getTime();
console.log(' antes:', data.toString());
// somando 24 horas
data.setTime(data.getTime() + (24 * millisPerHour));
depois = data.getTime();
console.log('depois:', data.toString());
console.log('diferença:', (depois - antes) / millisPerHour);
A data base é 1 de maio de 2017 (sim, o JavaScript tem esse problema irritante dos meses começarem em zero, por isso maio é 4
no código acima), às 10:00.
Primeiro eu somo 1 dia - como o JavaScript não tem (ainda) um jeito nativo de somar, esta é a forma de fazer. Depois somo a quantidade de milissegundos equivalente a 24 horas. Em ambos eu também pego a diferença entre antes e depois, e o resultado é o esperado, o mesmo horário do dia seguinte, com 24 de horas de diferença:
antes: Mon May 01 2017 10:00:00 GMT-0300 (Brasilia Standard Time)
depois: Tue May 02 2017 10:00:00 GMT-0300 (Brasilia Standard Time)
diferença: 24
antes: Mon May 01 2017 10:00:00 GMT-0300 (Brasilia Standard Time)
depois: Tue May 02 2017 10:00:00 GMT-0300 (Brasilia Standard Time)
diferença: 24
Agora o que acontece se mudarmos a data inicial para um dia antes do DST Gap?
var millisPerHour = 3600 * 1000;
var data = new Date(2017, 9, 14, 10, 0);
var antes = data.getTime();
console.log(' antes:', data.toString());
data.setDate(data.getDate() + 1);
var depois = data.getTime();
console.log('depois:', data.toString());
console.log('diferença:', (depois - antes) / millisPerHour);
data = new Date(2017, 9, 14, 10, 0);
antes = data.getTime();
console.log(' antes:', data.toString());
data.setTime(data.getTime() + (24 * millisPerHour));
depois = data.getTime();
console.log('depois:', data.toString());
console.log('diferença:', (depois - antes) / millisPerHour);
Agora a saída é:
antes: Sat Oct 14 2017 10:00:00 GMT-0300 (Brasilia Standard Time)
depois: Sun Oct 15 2017 10:00:00 GMT-0200 (Brasilia Summer Time)
diferença: 23
antes: Sat Oct 14 2017 10:00:00 GMT-0300 (Brasilia Standard Time)
depois: Sun Oct 15 2017 11:00:00 GMT-0200 (Brasilia Summer Time)
diferença: 24
No primeiro caso ele manteve o mesmo horário do dia seguinte. Mas por causa da mudança de offset, a diferença entre as datas é de 23 horas. No segundo caso, a diferença é de 24 horas, mas o horário não é o mesmo (repare também como mudou de "Standard Time" para "Summer Time", e o offset mudou de -0300
para -0200
).
Para entender melhor: imagine que no dia 14 às 10:00 eu inicio a contagem em um cronômetro. Às 23:59 o cronômetro estará marcando 13 horas e 59 minutos. À meia-noite ocorre o DST Gap, então o horário local passa a ser 1 da manhã. Mas o cronômetro não faz esse salto (ele não vai contar uma hora a mais), então às 01:00 do dia 15 ele estará marcando que se passaram 14 horas. Isso quer dizer às 10:00 do dia 15, o cronômetro indicará que se passaram 23 horas. Somente às 11:00 é que terão se passado 24 horas.
É um corner case que só vai acontecer duas vezes por ano, e se o horário de verão voltar? Sim, mas é algo que pode afetar seu sistema. Por exemplo, se vc tiver que medir quanto tempo determinada operação demorou, e levar em conta apenas a data e hora e desconsiderar o fuso (e consequentemente o offset), obterá resultados errados. Se tiver que lidar com datas antigas, tem que considerar se na época o horário de verão estava ou não em vigor, etc.
E caso tenha que somar datas por qualquer motivo ("O prazo para X é de N dias a partir da data D"), pode ser que faça diferença saber se "somar 1 dia" quer dizer "considerar o mesmo horário do dia seguinte", ou "24 horas do cronômetro" (ou ainda, se o sistema vai desconsiderar o horário, situação na qual nada disso se aplica).
Agora usando o mesmo exemplo para quando ocorre o DST Overlap:
var millisPerHour = 3600 * 1000;
var data = new Date(2017, 1, 18, 10, 0);
var antes = data.getTime();
console.log(' antes:', data.toString());
data.setDate(data.getDate() + 1);
var depois = data.getTime();
console.log('depois:', data.toString());
console.log('diferença:', (depois - antes) / millisPerHour);
data = new Date(2017, 1, 18, 10, 0);
antes = data.getTime();
console.log(' antes:', data.toString());
data.setTime(data.getTime() + (24 * millisPerHour));
depois = data.getTime();
console.log('depois:', data.toString());
console.log('diferença:', (depois - antes) / millisPerHour);
Saída:
antes: Sat Feb 18 2017 10:00:00 GMT-0200 (Brasilia Summer Time)
depois: Sun Feb 19 2017 10:00:00 GMT-0300 (Brasilia Standard Time)
diferença: 25
antes: Sat Feb 18 2017 10:00:00 GMT-0200 (Brasilia Summer Time)
depois: Sun Feb 19 2017 09:00:00 GMT-0300 (Brasilia Standard Time)
diferença: 24
Novamente, o primeiro caso manteve o mesmo horário, mas por causa da mudança de offset (de -0200
para -0300
, ou seja, do horário de verão para o "horário normal"), a diferença é de 25 horas. Já somando 24 horas, o resultado foi um horário diferente.
Aqui vale a mesma explicação: imagine que no dia 18 às 10h eu inicio o cronômetro. Às 23:59 ele estará marcando 13 horas e 59 minutos. À meia-noite, graças ao DST Overlap, o relógio volta para 23:00 - isso quer dizer que todos os minutos entre 23:00 e 23:59 serão contados duas vezes pelo cronômetro: uma no horário de verão, e outra no horário "normal". Por isso que às 10:00 do dia 19, o cronômetro estará marcando que se passaram 25 horas.
Vale lembrar que gaps e overlaps nem sempre são de uma hora (há regiões da Austrália que durante o horário de verão adiantam o relógio em meia hora), e nem sempre é por causa do horário de verão (em 2018 a Coreia do Norte adiantou seu fuso em meia-hora para alinhá-lo com o horário da Coreia do Sul).
Lembre-se que quem define as regras de qualquer fuso horário (se vai ou não ter horário de verão, quando começa e termina, qual offset será usado, etc) é o governo local de cada região, muitas vezes sem justificativa técnica (é comum dizer que "o povo terá mais horas de sol" e coisas do tipo), então mesmo se hoje não tiver horário de verão na sua região, nada garante que amanhã não haverá. Ao somar datas ou calcular a diferença entre elas, esses fatores sempre devem ser levados em consideração, pois pode dar diferença dependendo da forma como você calcula.
Este texto é baseado neste post do meu blog. Lá os exemplos são em Java, usando o java.time
(nova API de datas do Java 8), mas a ideia principal é a mesma.
As imagens do DST Gap e DST Overlap foram retiradas deste livro, com a devida permissão do autor - que sou eu mesmo :-)