3

I have a list containing 3 objects, sorted by modification date, such as:

{id=1, name=apple, type=fruit, modificationDate=2019-09-02}
{id=2, name=potato, type=vegetable, modificationDate=2019-06-12}
{id=3, name=dog, type=animal, modificationDate=2018-12-22}

What I need to do is to filter those items in such way: an object of type animal will be always pass to new list, but only one item of type fruit and vegetable (the one with most recent date of modification) will be passed, so the result list would be the following:

{id=1, name=apple, type=fruit, modificationDate=2019-09-02}
{id=3, name=dog, type=animal, modificationDate=2018-12-22}

I tried to combine findFirst() and filter() on stream operation, altough, I only managed to make it work one after another, not as 'or' conditions.

It works with such a code:

    List<Item> g = list.stream().filter(f -> f.getType() == animal).collect(Collectors.toList());
    Item h = list.stream().filter(f -> f.getType() != animal).findFirst().get();

    g.add(h);

But it's extremaly ugly solution, so I'm looking for something more elegant.

Any help appreaciated!

PS. The list will always contain only 3 items, 1 of type animal, which should stay and 2 items of different types, sorted descending by modification date.

3
  • Are the modification dates unique? Commented Sep 2, 2019 at 13:02
  • Yes, they are unique Commented Sep 2, 2019 at 13:02
  • I had edited my question, the list will always contain 3 items, sorted in descending order by modification date Commented Sep 2, 2019 at 13:11

4 Answers 4

3

You can use Stream.concat to join the two streams of Item one having type animal and the other of non-animal type. Then filter and sort the stream having the non-animal type Item in descending order of modificationDate and take the first one by putting a limit(1).

Then finally when the Stream is merged you sort it again, this time between the the stream of Item of animal type and stream of Item of non-animal type:

Comparator<Item> comp = Comparator.comparing(Item::getModificationDate, Comparator.reverseOrder());
List<Item> sorted = Stream.concat(list.stream()
                                      .filter(f -> "animal".equals(f.getType())), 
                                  list.stream()
                                      .filter(f -> !"animal".equals(f.getType()))
                                      .sorted(comp)
                                      .limit(1))
                             .sorted(comp)
                             .collect(Collectors.toList());

System.out.println(sorted);

I have assumed that modificationDate is a LocalDateTime field, otherwise if it is a String then the sorting will happen in the lexical order.

Sign up to request clarification or add additional context in comments.

2 Comments

"I have assumed that modificationDate is a LocalDateTime field, otherwise if it is a String then the sorting will happen in the lexical order." If the date strings are in the form shown, using the one and only correct string representation of a date yyyy-mm-dd, lexical order will be the same as date order.
.sorted(comp) .limit(1) is an expensive way to express min(comp). Starting with Java 9, you can use min(comp).stream() to convert the optional back to a stream of one or zero items. Java 8 requires something like min(comp).map(Stream::of).orElseGet(Stream::empty). Still preferable over sorting an arbitrary number of items, just to get one of them.
1

what if you will try to find an unwanted item an then will remove it from the list instead of complicated filtering? I guess this works nice:

@Test
  public void testTest() {
    List<TestClass> list = new ArrayList<>(Arrays.asList(new TestClass("animal", new Date())
        , new TestClass("fruit", DateUtils.addDays(new Date(), -2))
        , new TestClass("veg", DateUtils.addDays(new Date(), -1))));
    TestClass toRemove = list.stream().filter(item -> !Objects.equals("animal", item.getName()))
        .min(Comparator.comparing(TestClass::getDate)).get();
    list.remove(toRemove);

    System.out.println(list);
  }

  class TestClass {
    private String name;
    private Date date;

    public TestClass(String name, Date date) {
      this.name = name;
      this.date = date;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public Date getDate() {
      return date;
    }

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

    @Override public String toString() {
      final StringBuilder sb = new StringBuilder("TestClass{");
      sb.append("name='").append(name).append('\'');
      sb.append(", date=").append(date);
      sb.append('}');
      return sb.toString();
    }
  }

Comments

1

What I need to do is to filter those items in such way: an object of type animal will be always pass to new list, but only one item of type fruit and vegetable (the one with most recent date of modification) will be passed, so the result list would be the following:

If I understand this right, what you're looking for is (simplified): from a given list, obtain an item of animal type and an item of non-animal type, keeping only those with maximum last modified date.

I'm going to play on a side implementation detail of the animal item always being the single one in the list. What that means is, you can partiton stream with a maxBy downstream, and this both keep the animal item in, and it will find the correct non-animal item:

List<Item> input = ... // your list here

// a small inconvenience with types here, because maxBy's
// reduction type is Optional
Map<Boolean, Optional<Item>> map = list.stream()
    .collect(Collectors.partitioningBy(
            item -> "animal".equals(item.getType()),
            Collectors.maxBy(Comparator.comparing(Item::getLastModifiedDate))
        )
    );

List<Item> result = new ArrayList<>(2);
map.values().forEach(option -> option.ifPresent(result::add));

consume(result);

Note: on my machine it puts non-animals first into the list, because of how partitioning collector's result is iterated over (that map's first entry is for Boolean.FALSE). You can reverse the partitioning predicate to make it so the animal comes first, without chaning anything else in the code.

Comments

1

Another approach would be to sort the list twice. First sort the list so that the element of type animal is always at the beginning of the list then the others are listed by modification date. Then limit the list to two elements and sort again by modification date. I'm using here that "animal" is alphabetically before the other types, but you could also implement a simple logic which makes the animal < fruit / vegetables

Function<Item,Integer> byType = i -> i.type.equals("animal")?1:0;
List<Item> myList = list.stream()
                        .sorted(Comparator.comparing(byType)
                                .thenComparing(Item::getModificationDate).reversed())
                        .limit(2)
                        .sorted(Comparator.comparing(Item::getModificationDate).reversed())
                        .collect(Collectors.toList());

The advantage here is that it can be done in one step.

4 Comments

This loses part that it's required to get the most recently modified "fruit/veg". Since you limited by two, you'll always get the fruit before veg, even though veg might've been modified later.
@M.Prokhorov Thank you for the note. I've added a function which solves the issue.
A boolean is also a Comparable, so there's no direct need to convert to byte. Just need to check which way around it's ordered. I think true comes first, but I don't remember.
You solution only works under the assumption that there are only the three items of the question’s example, which is a bit pointless. If the input was fixed to these three items, you could simply pick the desired two from the list. Of course, a solution should also handle scenarios where more items of either type exist.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.