deHaar
deHaar

Reputation: 18568

How to get a sublist starting with a certain match and ending with a different match by Java stream API?

I am interested in a streamy way to get a sublist out of an ordered list of objects. The sublist should start with an object matching a given condition concerning one of its attributes and it should end with an object matching a different condition.

Let's say, I have the following class Element:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Element implements Comparable<Element> {

    String abbreviation;
    LocalDateTime creationTime;

    public Element(String abbreviation, LocalDateTime creationTime) {
        this.abbreviation = abbreviation;
        this.creationTime = creationTime;
    }

    public String getAbbreviation() {
        return abbreviation;
    }

    public void setAbbreviation(String abbreviation) {
        this.abbreviation = abbreviation;
    }

    public LocalDateTime getCreationTime() {
        return creationTime;
    }

    public void setCreationTime(LocalDateTime creationTime) {
        this.creationTime = creationTime;
    }

    @Override
    public int compareTo(Element otherElement) {
        return this.creationTime.compareTo(otherElement.getCreationTime());
    }

    @Override
    public String toString() {
        return "[" + abbreviation + ", "
                + creationTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + "]";
    }
}

I receive a List<Element> which is ordered by creationTime. Now I want to find out if there are elements between two specific ones only regarding their abbreviation. I know how to filter single elements or lists of elements matching a given abbreviation using findFirst() or findAny() and Collectors.toList(). However, I just don't know how to find a sublist beginning with a certain abbreviation and ending with another one because the elements in between do (and should) not match the conditions and there are two different conditions.

Assume I have the following List<Element>(-generating method):

private static List<Element> generateSomeElements() {
    List<Element> elements = new ArrayList<Element>();

    LocalDateTime now = LocalDateTime.now();

    Element elementOne = new Element("ABC", now.minus(14, ChronoUnit.DAYS));
    Element elementTwo = new Element("DEF", now.minus(13, ChronoUnit.DAYS));
    Element elementThree = new Element("GHI", now.minus(12, ChronoUnit.DAYS));
    Element elementFour = new Element("JKL", now.minus(11, ChronoUnit.DAYS));
    Element elementFive = new Element("MNO", now.minus(10, ChronoUnit.DAYS));
    Element elementSix = new Element("PQR", now.minus(9, ChronoUnit.DAYS));
    Element elementSeven = new Element("STU", now.minus(8, ChronoUnit.DAYS));
    Element elementEight = new Element("VWX", now.minus(7, ChronoUnit.DAYS));
    Element elementNine = new Element("YZ1", now.minus(6, ChronoUnit.DAYS));
    Element elementTen = new Element("234", now.minus(5, ChronoUnit.DAYS));

    elements.add(elementOne);
    elements.add(elementTwo);
    elements.add(elementThree);
    elements.add(elementFour);
    elements.add(elementFive);
    elements.add(elementSix);
    elements.add(elementSeven);
    elements.add(elementEight);
    elements.add(elementNine);
    elements.add(elementTen);

    return elements;
}

I need the sublist from (including) "MNO" to (including) "YZ1" keeping the order it has in the source list, leaving the creationTime aside.

I know how to do that without using streams (like finding indexes of start and end and then getting the elements from index start to index end), which might be sufficient but is somehow old-fashioned and not really satisfying my greed for utilizing modern APIs.

I could add the ways I made it work, but they would just be another form of describing the problem which I am not able to resolve using a single stream-statement. In addition, those ways are explained in a multitude of different questions asking for a general possibility and not targeting stream API.

In general:
There are two conditions and a List<Element> where a sublist is to be collected whose first element is the one matching condition 1 and whose last element it the one matching condition 2 while all elements between those are collected, too, and the order stays as it is in the origin.

So if you know a possibility of getting the desired sublist with a single stream API statement, please add it as an answer.

I will have a look at the reduce() method in the meantime, maybe that is the answer to the mystery…

EDIT
I found a solution which gives me the desired result using stream() calls, but I need three of them. One which fetches the element that matches the first condition, a second one that finds the matching element for the second condition and then one which fetches the desired sublist using the creationTimes of the two elements that were found in the first two stream()s:

public static void main(String[] args) {
    List<Element> elements = generateSomeElements();

    // print origin once followed by a horizontal separator for the human eye
    elements.forEach(element -> System.out.println(element.toString()));

    System.out.println("————————————————————————————————");

    // find the reference object which should be the first element of the desired
    // sublist
    Element fromIncluding = elements.stream()
            .filter(element -> element.getAbbreviation().equals("MNO")).findFirst()
            .get();
    Element toIncluding = elements.stream()
            .filter(element -> element.getAbbreviation().equals("YZ1")).findFirst()
            .get();

    List<Element> mnoToYz1 = elements.stream()
            .filter(element ->
                (element.getCreationTime().isAfter(fromIncluding.getCreationTime())
                    && element.getCreationTime().isBefore(toIncluding.getCreationTime()))
                || element.getCreationTime().isEqual(fromIncluding.getCreationTime())
                || element.getCreationTime().isEqual(toIncluding.getCreationTime()))
            .collect(Collectors.toList());

    mnoToYz1.forEach(element -> System.out.println(element.toString()));
}

Upvotes: 3

Views: 1615

Answers (2)

Naman
Naman

Reputation: 31878

If the input list is ordered, another way to get the subList would be using the range instead of the object comparison in your example as :

int fromIncluding = IntStream.range(0, elements.size())
    .filter(i -> elements.get(i).getAbbreviation().equals("MNO"))
    .findFirst().orElse(0);
int toIncluding = IntStream.range(fromIncluding, elements.size())
    .filter(i -> elements.get(i).getAbbreviation().equals("YZ1"))
    .findFirst().orElse(elements.size() - 1);

List<Element> mnoToYz1 = elements.subList(fromIncluding, toIncluding+1);

Upvotes: 1

Sergii Lagutin
Sergii Lagutin

Reputation: 10661

Looks like you need takeWhile / dropWhile methods on the stream. Take a look at StreamEx library by Tagir Valeev

Upvotes: 1

Related Questions