Streams e datas para os desafios do dia a dia no Java 8

Já falamos sobre algumas das novidades do Java 8, como lambdas, streams, default methods e method references e a nova API de data/hora.

Que tal juntar tudo isso pra resolver um probleminha bacana?

Imagine que queremos gerar uma lista com todos dias úteis de um determinado mês. Para o nosso problema, consideramos que dia útil é qualquer dia que não estiver no final de semana, ou seja, sem contar feriados.

Gerando dias úteis da maneira clássica

Recapitulando, a API java.time tem uma classe que representa o período de um dia (LocalDate) e uma classe que representa um mês inteiro (YearMonth).

Para obter o mês de maio de 2014, faríamos:

YearMonth anoMes = YearMonth.of(2014, 5);

Poderíamos obter a duração desse mês:

anoMes.lengthOfMonth(); // 31

Também seria possível obter um dia específico desse mês:

LocalDate data = anoMes.atDay(28); // 28/05/2014

Com o dia em mãos, poderíamos descobrir o dia da semana:

data.getDayOfWeek(); // DayOfWeeek.WEDNESDAY

Utilizando os recursos acima, podemos gerar a lista de dias úteis do mês/ano percorrendo com um for do primeiro ao último dia do mês e adicionando a uma lista os dias que não são sábado nem domingo.

Juntando tudo, nossa solução iterativa ficaria assim:

int ano = 2014;
int mes = 5;
YearMonth anoMes = YearMonth.of(ano, mes);

List<LocalDate> listaDosDiasUteisDoMes = new ArrayList<>();

//gerando
for(int dia=1; dia <= anoMes.lengthOfMonth(); dia++){ 
	LocalDate data = anoMes.atDay(dia); 

	//filtrando
	if(!data.getDayOfWeek().equals(DayOfWeek.SATURDAY) && 	
		!data.getDayOfWeek().equals(DayOfWeek.SUNDAY)){

			//coletando
			listaDosDiasUteisDoMes.add(data);
	}
}

Observe que geramos os dias com o for e o método atDay.

Depois disso, filtramos os dias úteis com a condição de dentro do if.

Finalmente, coletamos os dias úteis em uma lista com o método add de List.

Gerando dias úteis com lambdas e streams

Mas e se utilizarmos os novos recursos do Java 8? Como ficaria nossa solução?

Nosso primeiro passo é, dado um mês, gerar todos os dias possíveis para aquele mês.

A classe Stream possui um método chamado iterate que, dados um valor inicial e um lambda de incremento, retorna uma sequência infinita de valores, sucessivamente.

Por exemplo, para gerar uma sequência (mais especificamente, uma Stream) com todos os números inteiros positivos, poderíamos fazer:

Stream<Integer> inteirosPositivos = Stream.iterate(1, n -> n + 1);

Mas como trabalhar com essa sequência infinita? Poderíamos pegar os primeiros 10 inteiros positivos utilizando o método limit da classe Stream:

inteirosPositivos.limit(10);

Interessante, não? Refletindo um pouco, podemos dizer que os meses começam no dia 1º e vão sendo incrementados, limitando-se ao número de dias do mês.

Se tivermos uma variável chamada anoMes que contém um YearMonth representando um mês qualquer, poderíamos gerar todos os dias desse mês com o seguinte código:

Stream<LocalDate> todosOsDiasDoMes = 
    Stream.iterate(anoMes.atDay(1),  data -> data.plusDays(1))
          .limit(anoMes.lengthOfMonth());

Observe acima que o valor inicial passado para o método iterate foi o dia 1º do mês. Já o lambda de incremento utilizou o método plusDays de LocalDate para adicionar um dia.

Com os dias do mês na mão, podemos utilizar o método filter da classe Stream. Esse método recebe um lambda que encapsula uma condição e cria um novo Stream com apenas os elementos que atendem a condição.

No nosso caso, precisamos garantir que a data não é um sábado nem um domingo:

Stream<LocalDate> diasUteisDoMes = 
    todosOsDiasDoMes.filter(data -> 
        !data.getDayOfWeek().equals(DayOfWeek.SATURDAY) && 
        !data.getDayOfWeek().equals(DayOfWeek.SUNDAY));

Agora, temos uma Stream com apenas os dias úteis do mês. Precisamos coletar o resultado em uma lista. Para isso, utilizaremos o método collect de Stream, que recebe um Collector. Utilizaremos um coletor já existente na classe auxiliar Collectors.

List<LocalDate> listaDosDiasUteisDoMes = 
    diasUteisDoMes.collect(Collectors.toList());

Colocando todo o código junto e omitindo variáveis auxiliares, teríamos:

int ano = 2014;
int mes = 5;
YearMonth anoMes = YearMonth.of(ano, mes);

List<LocalDate> listaDosDiasUteisDoMes =
    Stream.iterate(anoMes.atDay(1),  data -> data.plusDays(1))
                                .limit(anoMes.lengthOfMonth())
        .filter(data -> !data.getDayOfWeek().equals(DayOfWeek.SATURDAY) && 
                            !data.getDayOfWeek().equals(DayOfWeek.SUNDAY))
        .collect(Collectors.toList());

Observe que os passos do nosso algortimo (gerar valores, filtrá-los e coletar os resultados) foram facilmente mapeados para métodos da classe Stream. Fizemos uma sequência de operações (ou pipeline) que foram retornando novas instâncias de Stream, até coletarmos os resultados, terminando nossa computação .

Poderíamos isolar o código acima em uma classe e utilizá-la em vários pontos de uma aplicação. Seria interessante expandir a solução para gerar todos os dias úteis de um ano e, quem sabe, ler os feriados de um arquivo ou WebService.

Com lambdas e streams, podemos fazer sequências de operações de maneira natural e elegante. E manipular datas com a API java.time é simples e fácil.

E você? Já está pronto para usar os novos recursos do Java 8 nos seus projetos?

17 Comentários

  1. Sérgio Lopes 16/06/2014 at 16:22 #

    Bem legal a mistura!

  2. Michael Nascimento Santos 17/06/2014 at 00:12 #

    Eu prefiro fazer com IntStream.rangeClosed(1, anoMes.lengthOfMonth()).map(dia -> anoMes.atDay(dia)) pra gerar os dias do mês, devido a impl funciona melhor por enquanto. Além disso, recomendo usar um EnumSet para os dias da semana e validar com um !finalDeSemana.contains(day.getDayOfWeek()).

  3. Alexandre Aquiles 17/06/2014 at 09:32 #

    Excelentes sugestões, Mr. M!

    Realmente, usar uma range a partir de IntStream parece mais natural. O interessante do Stream.iterate é introduzir o conceito de lazyness e sequências infinitas.

    E, sem dúvida, utilizar um EnumSet iria deixar a lambda do filter mais limpa.

  4. Carlos Eduardo 17/06/2014 at 12:28 #

    Legibilidade zero. Coitado do programador que pegar estes códigos pra dar manutenção daqui uns 5 anos.

  5. Alexandre Aquiles 17/06/2014 at 12:36 #

    Carlos,
    Legilibidade é algo que depende de contexto, né? Se a sua equipe estiver acostumada com streams e lambdas, talvez código não seja assim tão complicado.

  6. Paulo Silveira 17/06/2014 at 18:17 #

    Tenho a mesma opinião do Aquiles. Lembro de quando eu reclamava do generics. De que atrapalharia a legibilidade do código. E hoje é muito pelo contrário.

    É bem mais fácil ler um código assim que ifs e fors encadeados a exaustão.

  7. rodolfo 21/06/2014 at 15:39 #

    E em relação aos vários imports estaticos (ou são apenas para fins didáticos)?

    Nao prejudicam a testabilidade?

  8. Paulo Silveira 21/06/2014 at 16:24 #

    verdade rodolfo, mas metodos estaticos sempre atrapalham testabilidade, ja que é dificil mocka-los, independente dos imposts.

  9. Alexandre Aquiles 21/06/2014 at 20:44 #

    Rodolfo,

    Acho que você está falando dos métodos estáticos da classe DiasUteis que está no gist (https://gist.github.com/alexandreaquiles/10300153), né?

    Realmente, o código é para efeito didático, sem cerimônias. Alguns dos métodos, talvez a maioria, ficariam melhores como de instância, certamente.

    Um interessante é o método diaUtil(), que retorna um lambda que é um teste em um LocalDate. Não me parece tão ruim esse design, em específico.

    Mas ainda não temos guias claros sobre o design de código que usa lambdas em Java . É um terreno a ser explorado!

  10. Douglas Arantes 25/06/2014 at 01:30 #

    Parabéns Alexandre pelo post.
    Estou lendo o livro “Funcitional Programming in Java” do Venkat Subramaniam, é um ótimo livro, o autor aborda de maneira muito didática o assunto e passa algumas dicas de design utilizando lambas e streams.

    Seria também uma boa hora de sair uma terceira edição do Effective Java.

  11. Alexandre Aquiles 25/06/2014 at 08:39 #

    Douglas,

    Li o livro do Venkat e, realmente, há boas dicas de design! Mas um Effective Java novo, com guias sobre como usar lambdas e stream, seria excelente! 🙂

  12. Raphael Lacerda 25/06/2014 at 15:04 #

    Excelente..
    e os feriados?

    Tem tipo um noWorkingDay?

  13. Alexandre Aquiles 25/06/2014 at 16:40 #

    O java.time não trata feriados direto na API. Aí, teríamos que usar uma biblioteca como as que você usou no post lá de 2012. O Object Lab Kit já suporta a JDK8. Ou podemos desenvolver a nossa! 😉

  14. Bernardo Sulzbach 14/09/2014 at 15:36 #

    E quanto a performance?

    Nesse caso, como os dados (dias úteis de cada mês de 1/x/x até (dia final)/y/y) poderiam ser gerados uma única fez, armazenados e consultados conforme fosse, quantos ciclos são utilizados para a geração desses dados é algo irrelevante.

    Porém, se as requisições fossem mais específicas e a implementação demandasse sempre a invocação de um método semelhante para produzir os resultados exigidos, iterar/selecionar/armazenar seria superior a (mais eficiente que) streams/lambdas?

    Grato, Sulzbach.

  15. Alexandre Aquiles 14/09/2014 at 15:59 #

    Bernardo,

    Evitamos colocar no post comparações de performance porque micro-benchmarks precisam ser muito bem feitos para evitar conclusões indevidas.

    Dito isso, nas medições que eu fiz para um problema ligeiramente diferente (gerar todos os dias úteis de um ano) o iterativo leva uma ligeira vantagem em relação a alternativas que usam streams e lambdas. Porém, a diferença não é tanta. Seria relevante se fosse uma ordem de magnetude maior (x10), mas não é o caso.

    O código desse esboço de comparação está em: https://gist.github.com/alexandreaquiles/10475548

  16. Mauricio 24/11/2014 at 14:04 #

    Bom galera, desenvolvi esse algoritmo que acredito ser mais eficiente, principalmente se usado para calcular longos periodos:

    public class DataUtil {

    public boolean isFinalDeSemana(LocalDate date){
    return date.getDayOfWeek() == SATURDAY || date.getDayOfWeek() == SUNDAY;
    }

    public long contaDiasUteisEntre(LocalDate start, LocalDate end){
    long dias;

    long finsDeSemana;

    dias = ChronoUnit.DAYS.between(start, end);

    long semanas = ChronoUnit.WEEKS.between(start, end);
    finsDeSemana = semanas * 2;

    dias = dias – finsDeSemana;

    //Elimina o caso de comparar duas datas que caem no fim de semana com o mesmo dia
    if((start.getDayOfWeek() == SATURDAY && end.getDayOfWeek() == SATURDAY)
    || (start.getDayOfWeek() == SUNDAY && end.getDayOfWeek() == SUNDAY)){
    dias–;
    }

    while(start.getDayOfWeek().compareTo(end.getDayOfWeek()) > 0){

    if(isFinalDeSemana(start)){
    dias–;
    }

    start = start.plusDays(1);
    }

    return dias;
    }
    }

  17. Jader Lucas 09/04/2017 at 11:14 #

    Caraca Mauricio, muito bom, acredito que poderia ser implementado este método diretamente na API.
    Para períodos longos com certeza é o mais eficiente.
    Para períodos pequenos pode até não ser, mas não deve usar muito mais ciclos do que as apresentadas.
    Ou seja, ele apresenta uma quantidade de ciclos que só depende dos feriados e não dos dias entre as datas.
    Excelente!

Deixe uma resposta