Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use two filters in stream for different transformations

I need to perform transformations only for a particular condition. I do this transformation:

// filter 1: less date - group by max date by groupId
        List<Info> listResult = new ArrayList<>(listInfo.stream()
                .filter(info -> info.getDate().getTime() < date.getTime())
                .collect(Collectors.groupingBy(Info::getGroupId, Collectors.collectingAndThen(
                        Collectors.reducing((Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2),
                        Optional::get))).values());

But for the condition when there is more than the specified date, I do not need to convert anything, I just need to return this data:

// filter 2: more date - nothing change in list
        List<Info> listMoreByDate = listInfo.stream()
                .filter(info -> info.getDate().getTime() >= date.getTime())
                .collect(Collectors.toList());

Next, to combine these two filters - I combine the two lists:

listResult.addAll(listMoreByDate);

My question is, can this be done in one stream? Because filter 2 is absolutely useless, it simply returns a list for this condition.

Is it possible to perform these transformations with one continuous expression?

My full code:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

public class App {
    public static void main(String[] args) throws ParseException {
        Info info1 = new Info(1L, getDateFromStr("2018-02-02T10:00:00"), 3L);
        Info info2 = new Info(2L, getDateFromStr("2018-02-02T12:00:00"), 3L);
        Info info3 = new Info(3L, getDateFromStr("2018-02-05T12:00:00"), 6L);
        Info info4 = new Info(4L, getDateFromStr("2018-02-05T10:00:00"), 6L);

        Date date = getDateFromStr("2018-02-03T10:10:10");

        List<Info> listInfo = new ArrayList<>();
        listInfo.add(info1);
        listInfo.add(info2);
        listInfo.add(info3);
        listInfo.add(info4);

        // filter 1: less date - group by max date by groupId
        List<Info> listResult = new ArrayList<>(listInfo.stream()
                .filter(info -> info.getDate().getTime() < date.getTime())
                .collect(Collectors.groupingBy(Info::getGroupId, Collectors.collectingAndThen(
                        Collectors.reducing((Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2),
                        Optional::get))).values());

        // filter 2: more date - nothing change in list
        List<Info> listMoreByDate = listInfo.stream()
                .filter(info -> info.getDate().getTime() >= date.getTime())
                .collect(Collectors.toList());

        listResult.addAll(listMoreByDate);

        System.out.println("result: " + listResult);
    }

    private static Date getDateFromStr(String dateStr) throws ParseException {
        return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(dateStr);
    }
}

class Info {
    private Long id;
    private Date date;
    private Long groupId;

    public Info(Long id, Date date, Long groupId) {
        this.id = id;
        this.date = date;
        this.groupId = groupId;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Long getGroupId() {
        return groupId;
    }

    public void setGroupId(Long groupId) {
        this.groupId = groupId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Info info = (Info) o;
        return Objects.equals(id, info.id) &&
                Objects.equals(date, info.date) &&
                Objects.equals(groupId, info.groupId);
    }

    @Override
    public int hashCode() {

        return Objects.hash(id, date, groupId);
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Info{");
        sb.append("id=").append(id);
        sb.append(", date=").append(date);
        sb.append(", groupId=").append(groupId);
        sb.append('}');
        return sb.toString();
    }
}
like image 366
FreeOnGoo Avatar asked Nov 05 '18 12:11

FreeOnGoo


People also ask

Can we use two filter in stream?

Combining two filter instances creates more objects and hence more delegating code but this can change if you use method references rather than lambda expressions, e.g. replace filter(x -> x.

What is the purpose of filter () method in streams?

The filter() function of the Java stream allows you to narrow down the stream's items based on a criterion. If you only want items that are even on your list, you can use the filter method to do this. This method accepts a predicate as an input and returns a list of elements that are the results of that predicate.


2 Answers

I can’t see anything simpler than

List<Info> listResult = Stream.concat(
    listInfo.stream()
        .filter(info -> info.getDate().getTime() < date.getTime())
        .collect(Collectors.toMap(Info::getGroupId, Function.identity(),
            BinaryOperator.maxBy(Comparator.comparing(Info::getDate))))
        .values().stream(),
    listInfo.stream()
        .filter(info -> info.getDate().getTime() >= date.getTime())
    )
    .collect(Collectors.toList());

as these two operations are fundamentally different. Building a Map in the first step is unavoidable, as it will be used to identify the items with equal getGroupId property.

That said, you should consider switching from using Date to the java.time API.

like image 145
Holger Avatar answered Oct 06 '22 16:10

Holger


Yes, you can merge the two conditions by using the partitioningBy collector as follows:

 List<Info> resultSet = 
      listInfo.stream()
              .collect(collectingAndThen(partitioningBy(info -> info.getDate().getTime() < date.getTime()),
                     map -> Stream.concat(map.get(true)
                            .stream()
                            .collect(toMap(Info::getGroupId,
                                    Function.identity(),
                                    (Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2))
                            .values().stream(), map.get(false).stream())
                            .collect(Collectors.toCollection(ArrayList::new))));

This essentially uses the partitioningBy collector to organise the elements in such a way that all the elements passing the criteria info.getDate().getTime() < date.getTime() aswell as where it's false i.e. where info -> info.getDate().getTime() >= date.getTime() is true into a Map<Boolean, List<T>>.

Further, we utilise the collectingAndThen collector to apply a finishing function upon the Map<Boolean, List<T>> returned by the partitioningBy collector, in this case we concatenate the result of the applying the logic of:

.collect(Collectors.groupingBy(Info::getGroupId, 
          Collectors.collectingAndThen(Collectors.reducing((Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2),
           Optional::get))))
.values();

which I've simplified to:

.collect(toMap(Info::getGroupId, Function.identity(), (Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2)))
.values();

with the elements returned where info.getDate().getTime() < date.getTime() returned false (map.get(false).stream()).

Finally, we collect the result into a ArrayList implementation with the toCollection collector.

like image 22
Ousmane D. Avatar answered Oct 06 '22 17:10

Ousmane D.