Java Stream: filtro com vários intervalos

9

Estou tentando filtrar um recurso e excluir alguns elementos com base em um campo. Para excluir, tenho um conjunto (que contém um ID que precisa ser excluído) e uma lista (contém vários intervalos de IDs que precisam ser excluídos). Eu escrevi a lógica abaixo e não estou satisfeito com a segunda lógica do filtro. Existe alguma maneira melhor de fazê-lo com o Java 8? Eu preciso fazer o mesmo para incluir intervalos também.

Set<String> extensionsToExclude = new HashSet<>(Arrays.asList("20","25","60","900"));
List<String> rangesToExclude = new ArrayList<>(Arrays.asList("1-10","20-25","50-70","1000-1000000"));
return directoryRecords.stream()
        .filter((directoryRecord) -> !extensionsToExclude.contains(directoryRecord.getExtensionNumber()))
        .filter((directoryRecord -> {
            Boolean include = true;
            for(String s : rangesToExclude) {
                String [] rangeArray = s.split("-");
                Integer extension = Integer.parseInt(directoryRecord.getExtensionNumber());
                if(extension <= Integer.parseInt(rangeArray[0]) && extension >= Integer.parseInt(rangeArray[1])) {
                    include = false;
                }
            }
            return include;
        }))
        .collect(Collectors.toList());

Obrigado :)

Yadvendra Rathore
fonte
3
Não use Booleanobjetos quando você só precisa de um booleanvalor. Embora aqui, a variável includeseja totalmente obsoleta. Quando a única alteração possível é de truepara false, você pode substituir include = false;por, return false;pois o resultado final já foi determinado. Em seguida, o return include;no final pode ser substituído return true;e a declaração da variável removida. E como directoryRecordnunca muda no loop, você pode mover o Integer extension = Integer.parseInt(directoryRecord.getExtensionNumber());antes do loop (e mudar Integerpara int).
Holger

Respostas:

9

Eu faria isso com uma Rangeclasse personalizada , algo como:

class Range {
    private long start;
    private long end;

    Range(String start, String end) {
        this.start = Long.parseLong(start);
        this.end = Long.parseLong(end);
    }

    Range(String range) {
        this(range.split("-")[0], range.split("-")[1]);
    }

    boolean inRange(long n) {
        returns start <= n && n <= end;
    }
}

O que tornará possível algo assim:

List<Range> ranges = rangesToExclude.stream()
                     .map(Range::new).collect(Collectors.toList());
return directoryRecords.stream()
        .filter((directoryRecord) -> !extensionsToExclude
                                    .contains(directoryRecord.getExtensionNumber()))
        .filter(directoryRecord -> ranges.stream()
                                    .noneMatch(r -> r.isInRange(directoryRecord)))
        .collect(Collectors.toList());

Pessoalmente, acho seu primeiro filtro bom o suficiente para ser preservado como está.

ernest_k
fonte
2
Não deveria ser noneMatchquando estamos falando rangesToExclude? E suponho que poderia haver uma solução ainda mais elegante com uma TreeSet<Range>
Holger
De fato, eu devia estar com sono.
ernest_k 29/01
@ernest_k Obrigado pela solução. Acho realmente elegante.
Yadvendra Rathore 29/01
4

Eu sugeriria similar à resposta de ernest_k com Range.

Mas nessa abordagem, você pode usar a coleção para criar List<Range>(isso "20"pode ser tratado como "20-20") e alterar a condição do filtro para usar a negação anyMatch.

List<Range> ranges = Stream.concat(extensionsToExclude.stream(), rangesToExclude.stream())
        .map(Range::creatRange).collect(Collectors.toList());

return directoryRecords.stream()
        .filter(directoryRecord -> !ranges.stream()
                .anyMatch(r -> r.isInRange(
                        Integer.parseInt(directoryRecord.getExtensionNumber()))
                ))
        .collect(Collectors.toList());
class Range {
    private int start;
    private int end;

    Range(String start, String end) {
        this.start = Integer.parseInt(start);
        this.end = Integer.parseInt(end);
    }

    static Range creatRange(String range) {
        if (range.contains("-")) {
            return new Range(range.split("-")[0], range.split("-")[1]);
        }
        return new Range(range, range);
    }

    boolean isInRange(int n) {
        return start <= n && n <= end;
    }
}

ATUALIZAR

A criação de List<Range> rangespode ser alterada para remover pontos do Set<String> extensionsToExcludeintervalo criado a partir de List<String> rangesToExclud. Em seguida, intervalos desnecessários não serão criados.

List<Range> ranges = rangesToExclude.stream().map(Range::creatRange)
        .collect(Collectors.toCollection(ArrayList::new));
extensionsToExclude.stream()
        .filter(v -> !ranges.stream()
                .anyMatch(r -> r.isInRange(Integer.parseInt(v))))
        .map(Range::creatRange)
        .forEach(ranges::add);
lczapski
fonte
0

você pode fazer uma pausa antecipada se a condição do intervalo for verdadeira, em vez de esperar que todas as entradas sejam avaliadas.

if(extension >= Integer.parseInt(rangeArray[0]) && extension <= Integer.parseInt(rangeArray[1])) {
                    return true;
                }

caso contrário, basta retornar false após o loop for.

Angel Koh
fonte