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 JodaTime, ObjectLab 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.
Programação, Mobile, Front-end, Design & UX, Infraestrutura e Business
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.
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/
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.
Grande felix! Ta em Brasilia ainda?
E qndo vc vai blogar sobre esse calculo do INSS?
@Demetrius! Que bacana! Boa jornada pra vc!
Caso o cálculo tenha mudança para o horário de verão no periodo desejado, isso será levado em consideração?
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!
@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);
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.
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. 😀
muito bom e útil, valeu pela pesquisa e post
Curti e copiei.
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
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.
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! =)
E eu removi a parte que eu tinha escrito do Midnight, que nao estava la no codigo do calculo de qualquer forma.
parabéns! sempre procurei isso e fiz na mao varias vezes. que bom que dá para configurar os feriados!
\o/
Bom artigo!
Opa. Massa esse post. Ajudou muito.
Congratulações!!!
http://Chbigstock.com/%E3%82%AF%E3%83%AD%E3%83%A0%E3%83%8F%E3%83%BC%E3%83%84-%E3%83%A1%E3%82%AC%E3%83%8D-category-43.
html
Saved as a favorite, I love your web site!
Realmente muito bom! Não conhecia tal biblioteca. Vou começar a usá-la nos meus projetos. Parabéns pelo artigo.
Estou com dúvida em apenas tirar o domingo e contar o sábado, me ajuda ?
Passou por aqui um programador Microsoft!
Belo artigo, abraço.
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!
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
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.
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….
Muito Obrigado!