Home > OS >  find the largest 3 shops using java stream
find the largest 3 shops using java stream

Time:10-22

I have a list of shop objects that are grouped by the item they have.

class Shop{
  String shopName;
  String item;
  int size;
...}

How can I get a list of the 3 biggest shops (or n biggest shops) for each item? ie. suppose I have

Shop("Walmart", "Hammer", 100);
Shop("Target", "Scissor", 30);
Shop("Walgreens", "Hammer", 300);
Shop("Glens", "Hammer", 500);
Shop("Walmart", "Scissor", 75);
Shop("Toms", "Hammer", 150);

I want to return a list of the top 3 shops grouped by item. I grouped the items but i am not sure how to iterate through the given Map or entryset...

public class Shop {
  int size;
  String item;
  String name;

  public Shop(int size, String item, String name){
    this.size = size;
    this.item = item;
    this.name = name;
  }



  //Return a list of the top 3 largest shops by item
  public static void main(){


    List<Shop> shops = new LinkedList<Shop>();


    Comparator<Shop> shopComparator = new Comparator<Shop>(){
      @Override
      public int compare(Shop f1, Shop f2) {
        return f1.getSize() < f2.getSize() ? 1 : -1;
      }
    };

    shops.stream().collect(groupingBy(Shop::getItem))
            .entrySet()
            .stream()
            .filter(entry -> entry.getValue().stream().map )
            .forEach(item -> item.getValue())//Stuck here
            ;
  }

}

CodePudding user response:

Well, you could take the following steps:

  • With groupingBy(Shop::getItem), you could create a map which sorts by the item, so your result would be a Map<String, List<Shop>>, where the list contains all shops with that item.

  • Now we need to sort the List<Shop> in reversed order, so the top items of the list are the shops with the largest size. In order to do this, we could use collectingAndThen as downstream collector to groupingBy.

    Collectors.collectingAndThen(Collectors.toList(), finisherFunction);
    

    Our finisher function should sort the list:

    list -> {
        Collections.sort(list, Comparator.comparing(Shop::size).reversed());
        return list;
    }
    

    This would result in a Map<String, List<Shop>>, where the list is sorted, highest size first.

  • Now the only thing we need to do, is limiting the list size to 3. We could use subList. I think subList throws an exception if the list contains less than 3 items, so we need to use Math.min(3, list.size()) to take this into account.

    list -> {
        Collections.sort(list, Comparator.comparing(Shop::size).reversed());
        return list.subList(0, Math.min(3, list.size()));
    }
    

The whole code then looks like this:

shops.stream()
    .collect(groupingBy(Shop::item, Collectors.collectingAndThen(Collectors.toList(), list -> {
        Collections.sort(list, Comparator.comparing(Shop::size).reversed());
        return list.subList(0, Math.min(3, list.size()));
    })));

Instead of 'manually' sorting the list and limiting it to 3, you could create a small class which automatically does this — both limit and sort the list upon adding elements.

CodePudding user response:

Not as fancy as MC Emperor but it seems to work. I started from the part you already did:

shops.stream().collect(Collectors.groupingBy(Shop::getItem))
        .entrySet().stream().map(entry -> {
            entry.setValue(entry.getValue().stream()
              .sorted(Comparator.comparingInt(s->-s.size))
              .limit(3) // only keep top 3
              .collect(Collectors.toList()));
            return entry;
    }).forEach(item -> {
        System.out.println(item.getKey() ":" item.getValue());
    });

CodePudding user response:

You can use groupingBy along with limit to get desired result:

import static java.util.stream.Collectors.*;

// Define the sort logic. reversed() applies asc order (Default is desc)
Comparator<Shop> sortBySize = Comparator.comparingInt(Shop::getSize).reversed();
int limit = 3; // top n items

var itemToTopNShopsMap = list.stream().collect(
        collectingAndThen(groupingBy(Shop::getItem),
                itemToShopsMap -> getTopNShops(sortBySize, itemToShopsMap, limit)));


static Map<String, List<Shop>> getTopNShops(Comparator<Shop> sortBy, Map<String, List<Shop>> inMap, int limit) {
    var returningMap = new HashMap<String, List<Shop>>();
    for (var i : inMap.entrySet()) {
        returningMap.put(i.getKey(),  i.getValue().stream().sorted(sortBy).limit(Long.valueOf(limit)).collect(toList()));
    }
    return returningMap;
}

We took following steps:

  1. Group the List by 'item'
  2. For each grouping, i.e., item to list of shops entry, we sort the list of shops by predefined sort logic and collect (limit) the top n results.

Note:
In static method getTopNShops, mutation of source map is avoided. We could have written this method as a stream, but the stream version may have been less readable than the foreach loop.

CodePudding user response:

The most important thing that you can learn about streams is that they aren't inherently "better" than equivalent approaches by any measure. Sometimes, they make code more readable, other times, less so. Use them to clarify your code, and avoid them when they obfuscate it.

This is a case where your code will be far more readable by using a collector for this purpose. Coding your own is fairly easy, and if you really want to understand streams better, I recommend it as a simple learning exercise.

Here, I'm using MoreCollectors.greatest() from the StreamEx library:

Comparator<Shop> bySize = Comparator.comparingInt(Shop::getSize);
Map<String, List<Shop>> biggestByItem
    = shops.stream().collect(groupingBy(Shop::getItem, greatest(bySize, 3)));

This isn't better because it's shorter; its better because complexity is factored out of the code, and hidden behind meaningful names that explain the behavior. Instead of littering your application with complex pipelines that need to be read, tested, and maintained independently, you have written (or referenced) a reusable collector with a clear behavior.

As I mentioned, there is a bit of a learning curve in understanding how the pieces of a Collector work together, but it's worth learning. Here's a possible implementation for this greatest() collector:

public static <T> Collector<T, ?, List<T>> greatest(Comparator<? super T> order, int limit) {
    if (limit < 1) throw new IndexOutOfBoundsException(limit);
    Objects.requireNonNull(order);

    Supplier<Queue<T>> supplier = () -> new PriorityQueue<>(order);
    BiConsumer<Queue<T>, T> accumulator = (q, e) -> collect(order, limit, q, e);
    BinaryOperator<Queue<T>> combiner = (q1, q2) -> {
        q2.stream().forEach(e -> collect(order, limit, q1, e));
        return q1;
    };
    Function<Queue<T>, List<T>> finisher = q -> {
        List<T> list = new ArrayList<>(q);
        Collections.reverse(list);
        return list;
    };
    Collector.Characteristics characteristics = Collector.Characteristics.UNORDERED;
    return Collector.of(supplier, accumulator, combiner, finisher, characteristics);
}

private static <T> void collect(Comparator<? super T> order, int limit, Queue<T> q, T e) {
    if (q.size() < limit) {
        q.add(e);
    } else if (!q.isEmpty() && order.compare(e, q.peek()) > 0) {
        q.remove();
        q.add(e);
    }
}

You may be interested in this related question and its answers.

  • Related