Adoro datas. Sério, é uma das áreas mais fascinantes da programação. Se não acreditam, vejam lá o filme que foi o chamado bug do ano 2000. Até arranjaram um acrónimo fixolas e tudo: Y2K. Mas sobre isso falarei noutro dia, ou então não – até porque vem aí um problema bem mais grave em 2038 (a que alguns já chamam Y2K38 – c’mon, really?).

Aqui há dias relembraram-me o quanto eu gosto de datas. Um tipo – que eu não conheço de lado nenhum – pediu ajuda num fórum para fazer uma função em C que calculasse o número de dias entre duas datas; provavelmente, e tendo em consideração o pedido, para um trabalho da escola, ou coisa que o valha. Devo dizer que fui um bocadinho totó e disponibilizei logo ali a função inteira, quando a ideia de um fórum não é fazer por, mas sim ajudar a fazer.

Seja como for, só me decidi ajudá-lo por considerar o problema interessante; não é algo que seja muito comum hoje em dia, até porque a maior parte das linguagens de programação tem bibliotecas específicas para trabalhar com datas, e esta função é disponibilizada de forma directa. Além disso, vi que as orientações que já lhe tinham dado não eram, nem de perto, nem de longe, o mais eficaz que se podia ser. O problema no meio disto tudo, como não podia deixar de ser, é a existência ou não de anos bissextos no intervalo. Como Arthur C. Clarke colocou de forma hilariante em 3001 – Odisseia Final, “um dia, um dos menores erros de Deus será corrigido e o ano terá 12 meses de 30 dias exactamente iguais”.

Vamos recordar, como aquecimento, como se detecta se um ano é bissexto ou não. Duma forma básica, um ano é bissexto se o resto da sua divisão por quatro for zero. E esta regra tem-nos servido razoavelmente. No caso de alguém com 120 ou mais anos me estar a ler, deverá estar a dizer por esta altura (aplique-se o ciciar derivado à placa mal presa) “oh, menino, mas 1900 não foi bissexto, como é?”. Pois não, é que há outra regra: um ano não é bissexto se o resto da sua divisão por 100 for zero. Claro que agora, toda a gente com mais de 16 anos está a dizer “oh, pá, mas 2000 foi bissexto!”. Certo; ainda falta a terceira regra: um ano é mesmo bissexto (e desta, é mesmo para cumprir), se o resto da sua divisão por 400 for zero. Resumindo e baralhando: um ano é bissexto se o resto da sua divisão por 400 for zero ou se o resto da sua divisão por quatro for zero e, por 100, diferente de zero.

Mas vamos ao cálculo da diferença, então. A pior sugestão que alguma vez ouvi foi a um amigo, com quem mantive uma discussão há uns anos sobre datas e programação, cuja sugestão envolvia o uso e manutenção de um vector com todos os anos bissextos existentes. Quando eu lhe fiz notar a imensidade a que se propunha, dando-lhe datas de interesse histórico, como o ano da fundação de Portugal, atirou-me à cara, não sem razão, que era um absurdo, visto que a bissextalidade da forma como a conhecemos hoje em dia, só foi instiuida no calendário Gregoriano, em vigor desde 1582. Contra-argumentei com possível interesse histórico e académico na diferença entre as datas usadas no calendário Juliano e Gregoriano e, finalmente, com os anos futuros. Retirei-me da conversa quando a resposta dele foi “quando se chegar ao limite do que está no vector, acrescenta-se mais umas centenas de anos bissextos”… Menos, por favor…

Outra sugestão, que era a tal que deram no fórum, seria percorrer todos os anos, desde o ano da data mais antiga até ao ano da data mais recente, e testá-los um a um pela sua bissextalidade; uma melhoria óbvia seria testar um a um até encontrar o primeiro e depois testar apenas de 4 em 4. Com ou sem melhorias, esta metodologia envolverá sempre iterações, o que não abona nada em favor da performance, e que fará com que intervalos maiores demorem mais tempo a ser calculados.

Qual é a solução que proponho? Calcular, de forma matemática, o número de dias correspondente a cada data, desde 1 de Janeiro de 0 (sim, ano 0). Sem iterações, apenas operações matemáticas. O maior problema para isso, é que o dia da bissextalidade é num mês horrível. Sério, Júlio César, em que raio estavas a pensar? Fevereiro? Porque não no fim do ano? Esse tem de ser o nosso primeiro passo, passar o dia problemático para o final do ano, e ajustar o ano da data para reflectir essa alteração. Então:

mesRecalculado = RestoDaDivisão( (mes + 9) / 12 );
// a seguinte divisão deve ser calculada com recurso a truncagem
// a parte decimal, se houver, é descartada - NÃO É um arredondamento
anoRecalculado = ano - mesRecalculado / 10;

Com este cálculo, Março passa a ser o primeiro mês do ano (na realidade, é o mês zero) e Fevereiro o último. Devido à divisão inteira, quando o mês pedido é Janeiro ou Fevereiro (que assumem como novo número, respectivamente, 10 e 11), o ano é reajustado como sendo o ano anterior. Com estes dois cálculos, todos os possíveis dias 29 de Fevereiro estão agora no final de cada ano.

Vamos então calcular o número de dias sem olhar a bissextalidade (365 dias num ano), depois somar 1 por cada 4 anos, subtrair 1 por cada 100 anos e somar de novo 1 por cada 400 anos. A bissextalidade está agora sob controlo.

Agora que temos os dias todos calculados em relação aos anos, vamos enfiar os meses na ordem certa; o número de ordem do mês que calculamos anteriormente é multiplicado pelos dias de um ano, retirando os meses de Janeiro e Fevereiro (isto é, 306 dias), a dividir por 10 (que são os meses que ficaram à frente de Janeiro e Fevereiro, lembram-se?). Finalmente, vamos somar um dia por cada mês de 31 dias existente no ano (são 7), e compensar para os 28 dias que Fevereiro, normalmente, tem, visto que a bissextalidade já foi acertada lá atrás. Como este acerto de 5 dias tem de entrar no reordenamento dos meses, tem de ser, também, dividido por 10, sendo somado ao resultado da multiplicação prévia.

Com os meses também já fora do caminho, resta-nos somar o dia da data e pronto… Será?

O que é certo é que estas contas todas, embora aparentem estar correctas à primeira vista, têm sempre um dia a mais do que é suposto. Nem queiram saber o que eu passei para chegar a esta conclusão – tive que fazer uma versão mais intuitiva do cálculo dos dias (percorrendo todos os anos para calcular a bissextalidade) e comparar uma com a outra em milhares de datas. Mas é isso. No final, é só subtrair um dia. Resulta sempre.

// todas as divisões são calculadas com recurso a truncagem
// a parte decimal, se houver, é descartada - NÃO É um arredondamento

// calcular sem anos bissextos
dias = 365 * anoRecalculado;

// 1ª regra da  bissextalidade
dias = dias + anoRecalculado / 4;

// 2ª regra da bissextalidade
dias = dias - anoRecalculado / 100;

// 3ª regra da bissextalidade
dias = dias + anoRecalculado / 400;

// reordenação e contabilização dos meses
// inclui acerto para os meses de 31 dias menos 2 de Fevereiro
dias = dias + (mesRecalculado * 306 + 5) / 10

// dias da data
dias = dias + dia;

// acerto final
dias = dias - 1;

// estas contas podem ser todas "encomboiadas" numa linha
// aqui estão separadas a bem da clareza

Para resolver o nosso problema inicial – calcular a diferença em dias entre duas datas, ainda se lembram? – é tão simples como efectuar este cálculo para ambas e subtrair uma pela outra. Nem sequer interessa qual é a mais antiga, desde que devolvam o valor absoluto do resultado.

Para finalizar, vamos verificar as limitações.

Em primeiro lugar, como deve ser óbvio, anos negativos vão dar valores negativos. Para o caso em concreto, diferença entre datas, até se come, visto que vai dar correcto. Por exemplo, 1 de Janeiro de 1 AC dará –365 dias, e 1 de Janeiro de 1 DC dará 365 dias. Logo, a mais antiga menos a mais recente dará –730 dias. Ah, e tal, dirão, mas costuma ser ao contrário, isto é, a mais recente menos a mais antiga – o que daria zero. Certo, mas tal como disse atrás, se devolverem o valor em absoluto estão sempre safos, e basta controlarem qual é a mais antiga para ultrapassarem esta limitação dos anos negativos.

Em segundo lugar, temos a limitação dos inteiros. Ao codar isto, o mais normal é usar-se o tipo Int, int ou Integer, o que normalmente quer dizer 32 bit, com sinal, o que vem a dar um limite máximo de 2.147.483.647. Mas chega e sobra. Esta função vai dar o seu último resultado válido precisamente no dia 8 de Dezembro do ano cinco milhões, oitocentos e setenta e nove mil, seiscentos e dez. Que, por incrível que possa parecer, é bissexto! E, sim, escrevi por extenso de propósito.

Qualquer dia – quando tiver mais tempo e menos sono – coloco o código em C nos projectos. Noutras linguagens, sobretudo de alto nível, não se justifica; usem as bibliotecas integradas da linguagem. Isto foi just for fun

Update:

Ficheiro executável e código fonte em C no projecto Dias Entre Datas.

Partilhar no Sapo Links Partilhar no del.icio.us Partilhar no Digg Partilhar no Twitter Partilhar no StumbleUpon Partilhar no MySpace Partilhar no Facebook

Comentários Deixar um comentário

 Categorias
 Arquivo
 Projectos em Destaque
 Últimas Postas no Blog
 Últimos Comentários do Blog