O eterno problema de calcular a diferença de dias entre duas datas em Java

Seu chefe te dá a seguinte missão: calcular a diferença de dias entre duas datas. Usando API do java.util.Date ou java.util.Calendar você até consegue dar um jeito. Um mês depois ele pede a você que calcule novamente essa diferença, mas desprezando sábados e domingos. E agora? O que fazer? E tem mais, um tempo depois ele pede que despreze também os feriados, dias abonados, e dias que não teve expediente (greve, recesso, comemorações), considerando assim apenas os dias úteis. É um problema relativamente corriqueiro, a questão é como solucioná-lo sem fazer malabarismos com as classes do java.util, que deixam bastante a desejar.

Uma solução prática para este problema é o uso do JodaTime, projeto criado justamente para facilitar a manipulação de datas em Java. A biblioteca voltou a figurar nas notícias pois foi anunciado recentemente que a API de Data será reformulada para o Java 8, sob a especificação JSR 310, fortemente inspirada pelo JodaTime, já que um dos spec leaders é o próprio criador.

A especificação já existe há um bom tempo, inclusive conta com a participação do Fábio Kung, que já explicou em 2007 como seria a proposta. Porém, enquanto o Java 8 não chega, para resolvemos o problema, além do JodaTime.

Inicialmente, para obter uma data, basta instanciar um DateTime, que possui uma API bem completa e tem a vantagem de ser imutável. Ela representa um instante no tempo.

DateTime dataInicial = new DateTime(2012, 12, 1, 12, 0);
DateTime dataFinal = new DateTime(2012, 12, 28, 12, 30);
DateTime feriado = new DateTime(2012, 12, 25, 12, 0);

Essa classe funciona muito bem com a API do java.util, possuindo métodos toDate e toCalendar, além de um construtor que pode receber Date e Calendar.

Para calcular quantos dias existem entre uma data e outra, é muito simples:

int dias = Days.daysBetween(dataInicial, dataFinal).getDays();

Days.daysBetween devolve um objeto do tipo Days, que implementa a importante interface Period.

Vamos começar a complicar. Podemos calcular, de maneira análoga, a diferência de horas usando a classe Hours.

System.out.println(
  Hours.hoursBetween(dataInicial, dataFinal).getHours());

Aqui desprezamos os minutos, retornando apenas a quantidade de horas como um valor inteiro. Podemos usar a classe Minutes com BigDecimal.

BigDecimal minutos =
  new BigDecimal(Minutes.minutesBetween(dataInicial, dataFinal).getMinutes());
BigDecimal horas =
  minutos.divide(new BigDecimal("60"), 2, RoundingMode.HALF_UP);
System.out.println(horas); // 648,50h

Caso a gente vá calcular a diferença das datas em diversas unidades de tempo (horas, dias, minutos, etc), podemos utilizar a classe Duration, que tem um construtor que recebe dois instants (como DateTime) e depois permite recuperar a diferença na unidade temporal que você desejar.
Agora temos que descontar os feriados e finais de semana do período. Utilizaremos outras duas bibliotecas, que são relacionadas ao projeto JodaTimeObjectLab Holiday Calculation e Jollyday.

Primeiro passo é descobrir quais são os feriados no Brasil para o ano corrente. Vamos usar o Jollyday, que já vem com algum feriados e você pode customizar outros em um arquivo separado.

HolidayManager gerenciadorDeFeriados =
  HolidayManager.getInstance(de.jollyday.HolidayCalendar.BRAZIL);
Set<Holiday> feriados = gerenciadorDeFeriados.getHolidays(new DateTime().getYear());
Set<LocalDate> dataDosFeriados = new HashSet<LocalDate>();
for (Holiday h : feriados) {
    dataDosFeriados.add(new LocalDate(h.getDate(), ISOChronology.getInstance()));
}

Segundo passo é dado a lista de feriados dataDosFeriados, vamos utilizar o calendário do ObjectLab Holiday Calculation para descobrir se uma determinada data é dia útil, baseado tanto nos feriados quanto nos fins de semana. Fazemos isso através do método isNonWorkingDay

// popula com os feriados brasileiros
HolidayCalendar<LocalDate> calendarioDeFeriados
  = new DefaultHolidayCalendar<LocalDate>(dataDosFeriados);

LocalDateKitCalculatorsFactory.getDefaultInstance()
  .registerHolidays("BR", calendarioDeFeriados);

DateCalculator<LocalDate> calendario =
  LocalDateKitCalculatorsFactory.getDefaultInstance().
    getDateCalculator("BR", HolidayHandlerType.FORWARD);

// true - sábado
calendario.isNonWorkingDay(new LocalDate(dataInicial));

// false
calendario.isNonWorkingDay(new LocalDate(dataFinal));

// true - natal
calendario.isNonWorkingDay(new LocalDate(feriado));

Por fim, agora temos que descobrir quantos dias não úteis existem no período e descontar das horas já calculadas. Vamos caminhando de data em data e perguntando se ela é ou não dia útil. Se não for, incrementamos diasNaoUteis:

int diasNaoUteis = 0;

LocalDate dataInicialTemporaria = new LocalDate(dataInicial);
LocalDate dataFinalTemporaria = new LocalDate(dataFinal);

while (!dataInicialTemporaria.isAfter(dataFinalTemporaria)) {
    if (calendario.isNonWorkingDay(dataInicialTemporaria)) {
       diasNaoUteis++;
    }
    dataInicialTemporaria = dataInicialTemporaria.plusDays(1);
}

Se quisermos calcular o número de horas úteis entre essas datas, basta subtrair o total de horas que já calculamos de 24*diasNaoUteis:

System.out.println("Horas finais líquidas: " +
  horas.subtract(new BigDecimal(24 * diasNaoUteis)));

O código pode ser um pouco mais eficiente. Há uma outra solução para não precisar fazer um loop que passe por todas as datas do nosso intervalo. Podemos chamar HolidayManager.getHolidays() passando o intervalo das nossas datas, recebendo um Set. Invocando o size desse conjunto, temos o número de feriados do intervalo. Também calculamos quantos fins de semana tivemos, utilizando Weeks.weeksIn no nosso intervalo. Por último, precisamos tomar cuidado e verificar quais feriados cairam em fins de semana, para não contá-los duas vezes.

E você, como costuma resolver estes problemas? Vale lembrar que JodaTime possui integração com Hibernate e pode ser facilmente configurada no seu projeto. Também é fundamental pensar nos casos particulares quando escrever seus testes de unidade. É muito fácil você cair em situações estranhas, em especial se sua data possui horas e minutos, além de anos bissextos, feriados que caem em fins de semana, etc.

31 Comentários

  1. Felix Coutinho 17/10/2012 at 13:05 #

    Lembrando que ainda pode existir várias outras formas de se calcular intervalos de datas. Por exemplo, no INSS o cálculo de período é feito de uma forma totalmente diferentes do tradicional, com aproximações, regressões e etc. O JodaTime ajudou bastante nesse sentido.

  2. Caio Ribeiro Pereira 17/10/2012 at 13:28 #

    Caso alguém passe por este problema em Javascript, o melhor framework tanto client-side como server-side (node.js) é o MomentJS

    http://momentjs.com/

  3. Demetrius Feijóo Campos 17/10/2012 at 13:31 #

    Muito bom o post. Eu sou iniciante em Java e essas dicas sempre me ajudam a saber o que fazer nas horas difíceis, quando o chefe quer algo complexo como os exemplos citados acima.

  4. Raphael Lacerda 17/10/2012 at 14:32 #

    Grande felix! Ta em Brasilia ainda?
    E qndo vc vai blogar sobre esse calculo do INSS?

    @Demetrius! Que bacana! Boa jornada pra vc!

  5. Rafael Oliveira 17/10/2012 at 16:53 #

    Caso o cálculo tenha mudança para o horário de verão no periodo desejado, isso será levado em consideração?

  6. Java Progressivo 17/10/2012 at 21:18 #

    A grande vantagem do Java é a comunidade e a comunicação dos participantes.

    Sempre tem alguém criando algo, resolvendo algum problema e compartilhando.

    Quando tiverem alguma dúvida, podem pesquisar que muito provavelmente alguém já teve também e um criaram uma API pra resolver facilmente o problema!

  7. Raphael Lacerda 18/10/2012 at 09:45 #

    @Rafael! Boa pergunta!
    Resposta sim! O JodaTime já trata esta questão.

    Para o cenário abaixo, sem horário de verão seria 24.50 e com é 23.5.

    DateTime dataInicial = new DateTime(2012, 10, 20, 12, 0);
    DateTime dataFinal = new DateTime(2012, 10, 21, 12, 30);

    final Minutes minutos = Minutes.minutesBetween(dataInicial, dataFinal);

    final BigDecimal minutosTotais = new BigDecimal(minutos.getMinutes());
    //poderia usar a classe Duration

    System.out.println(minutosTotais);

    BigDecimal horas = minutosTotais.divide(new BigDecimal(“60”), 2, RoundingMode.HALF_UP);
    System.out.println(horas);

  8. Raphael Lacerda 19/10/2012 at 12:01 #

    Com relação ainda ao horário de verão, tem o decreto do presidente de 2008 que define o início no terceiro domingo do mês e outubro e o fim no terceiro domingo de fevereiro. Entretanto, caso este final coincida com o Carnaval, é postergado para o próximo. Não sei dizer se a biblioteca em questão trata essa condição.

  9. Igor Queiroz 23/10/2012 at 11:28 #

    Cara, eu só lembro nos meus tempos de estagiário na Dataprev, fiquei encarregado de fazer uma lógica mirabolante para calcular um intervalo de tempo para um sistema de previdência, e sofri um bocado para chegar ao resultado ideal.

    Como o Felix Coutinho disse aí, o INSS tem umas abordagens bem diferentes do que as convencionais. Mas lembro que esse foi um dos melhores desafios que já tive o prazer de passar ao longo da minha carreira. 😀

  10. thiago 23/10/2012 at 12:21 #

    muito bom e útil, valeu pela pesquisa e post

  11. Laisson 23/10/2012 at 22:07 #

    Curti e copiei.

  12. Rodrigo Vieira 24/10/2012 at 10:50 #

    Eae Rapha, blz ?

    Ótimo artigo com exemplos práticos do JodaTime. Você sempre mencionava ele nas aulas e vendo alguns casos reais no seu artigo ficou mais evidente como ele ajuda.

    Lá no trabalho como o sistema que desenvolvemos tratava controle de acesso, então não fazia diferença se era dia útil ou feriado, chegou o fim do período: access denied. Só tivemos que fazer uns malabarismos por conta de uns componentes que devolviam as datas em Date e precisávamos fazer uns ajustes com Calendar, mas acredito que com JodaTime seria muito mais tranquilo.

    Abraço.
    Rodrigão

  13. Michael Nascimento Santos 30/10/2012 at 00:58 #

    Infelizmente, todas as soluções mostradas aqui estão quebradas:

    http://blog.michaelnascimento.com.br/2012/10/29/precisa-se-de-apis-de-data-e-hora-que-funcionem-ou-uma-resposta-a-o-eterno-problema-de-calcular-a-diferenca-de-dias-entre-duas-datas-em-java/

    toDateMidnight() é um método que deveria ser banido do Joda-Time, assim como o conceito de cronologia do LocalDate.

    Espero que a JSR-310 não tenha esses problemas, mas pra isso precisamos de ajuda da comunidade em peso. Vocês são um grupo de grandes talentos técnicos que não percebeu os problemas acima, logo a chance de termos cometido erros semelhantes na API que será padrão também é grande. Só com colaboração da comunidade é que poderemos evitar isso.

  14. Raphael Lacerda 30/10/2012 at 09:23 #

    Michael, ótimo ponto, só acho que você não leu o post completo e/ou não prestou atenção em quais variáveis usamos para atingir a solução.

    em relação ao toDateMidnight, foi apenas um show up/helloWorld da api para iniciantes antes de mostrar a solução. E sim, em caso de horário de verão ele lançaria uma IlegalArgumentException! =) E acho justo ele lançá-la, afinal é uma data/hora que não existe.

    Todavia, para solução do problema usamos a classe Minutes, que como havia dito em comentários anteriores, já vai levar em conta uma hora a menos do horário de verão.

    Com relação new LocalDate(2012,1,1), como havia citado no post, é preciso colocar as datas que ele não considera feriado no xml (http://jollyday.sourceforge.net/properties.html) ou se vc tiver usando somente o ObjectLab, um arquivo com as datas.
    Tive que fazer isso agora para um periodo de greve que não pode ser levado em conta como hora útil e sai do outro lado!

    E por fim, não duvido que existam problemas na API, o próprio Stephen já disse que a API tem problemas (http://blog.joda.org/2009/11/why-jsr-310-isn-joda-time_4941.html), afinal como vc mesmo disse, tratar com datas é bem complicado e se fosse fácil alguém já teria feita algo melhor. Citando o autor da biblioteca, “..Joda has design flaws, but it does the job that was designed for…”
    Com as experiências que o Joda concedeu, acredito que a API 310 tem a tendência de ser ser melhor. Não acredito fielmente nisso pq o JPA 1 está aí para me lembrar o contrário.

    E assim que vc tornar a sua API pública de cálculo de datas, divulgue-a aqui! Terei o maior prazer de usá-la e tenho certeza que será melhor! =)

  15. Paulo Silveira 30/10/2012 at 11:23 #

    E eu removi a parte que eu tinha escrito do Midnight, que nao estava la no codigo do calculo de qualquer forma.

  16. Roberto Shizuo 31/10/2012 at 01:58 #

    parabéns! sempre procurei isso e fiz na mao varias vezes. que bom que dá para configurar os feriados!

  17. Juh SanNt 08/11/2012 at 12:13 #

    \o/

  18. Abraão Isvi 19/11/2012 at 02:49 #

    Bom artigo!

  19. Pedro Ciarlini 04/03/2013 at 15:31 #

    Opa. Massa esse post. Ajudou muito.

    Congratulações!!!

  20. Edwar Saliba Júnior 08/05/2014 at 01:23 #

    Realmente muito bom! Não conhecia tal biblioteca. Vou começar a usá-la nos meus projetos. Parabéns pelo artigo.

  21. Lucas Soares 18/03/2015 at 15:50 #

    Estou com dúvida em apenas tirar o domingo e contar o sábado, me ajuda ?

  22. DD-Anonimo 14/10/2015 at 15:44 #

    Passou por aqui um programador Microsoft!
    Belo artigo, abraço.

  23. Vinicius 09/04/2016 at 21:19 #

    Boa noite amigo, achei seu algoritmo muito bom. Porém estou tendo um problema com o new LocalDate. Quando eu compilo aparece o erro:

    “Exception in thread “main” java.lang.IllegalArgumentException: No partial converter found for type: java.time.LocalDate
    at org.joda.time.convert.ConverterManager.getPartialConverter(ConverterManager.java:252)
    at org.joda.time.LocalDate.(LocalDate.java:415)”

    Saberia me dizer como contornar isso? Muito grato pelo apoio!

  24. Vinicius 09/04/2016 at 21:20 #

    Sendo mais preciso, o erro está nessa linha: “dataDosFeriados.add(new LocalDate(h.getDate(), ISOChronology.getInstance()));”

    Aparece a mensagem de erro que eu relatei.

    Grato

  25. Webert Ribeiro 29/08/2016 at 22:36 #

    Olá, boa noite.

    Estou com um problema relativo ao fim do horário de verão.

    No fim do horário, temos 2x 23:00, certo ?

    Estou desenvolvendo um sistema que usa a data de nascimento dos usuários.

    Se um usuário nasceu por exemplo, às 23:30h de 31 de março de 1932 (antes do fim) e outro usuário nasceu 23:30h de 31 de março de 1932 (depois do fim), como o Java trabalha essa situação ?

    Fiz um teste com o GregorianCalendar e para o dia 3 de outubro de 1931 as 11h (inicio do horário verão) , usei o método getTimeInMillis para capturar o tempo, e depois converti para data novamente com o SimpleDateFormat, a saída foi 03/10/1931 – 12:00, ou seja, Java computou que a data marcava inicio do horário de verão e somou +1hora no horário.

    Agora, como trabalhar com o fim do horário de verão?

    Agradeço pela atenção, obrigado.

  26. Valdiney 26/12/2016 at 00:47 #

    Caso alguém queira também os Feriados Municipais, esse site devolve um XML com todos os feriados das cidades brasileiras:

    http://www.calendario.com.br/api_feriados_municipais_estaduais_nacionais.php

    Mas depois precisa escreve o código pra processar isso como desejarem….

Deixe uma resposta