Schroedinger
Schroedinger

Reputation: 341

Working with nested maps using Stream API

My current approach exploiting Streams API in conjunction with forEach loop:

public Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product,Integer>> shopping) {

    Map<String, Client> result = new HashMap<>();

    Map<Client, Map<String, BigDecimal>> temp =
            shopping.entrySet()
                    .stream()
                    .collect(Collectors.groupingBy(Map.Entry::getKey,
                             Collectors.flatMapping(e -> e.getValue().entrySet().stream(),
                             Collectors.groupingBy(e -> e.getKey().getCategory(),
                             Collectors.mapping(ee -> ee.getKey().getPrice().multiply(BigDecimal.valueOf(ee.getValue())),
                             Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))))));

    /*curious, how could I refactor that piece of code, so the method uses only one stream chain? */
    temp.forEach((client, value) 
        -> value.forEach((category, value1) 
        -> {
               if (!result.containsKey(category) ||
                   temp.get(result.get(category)).get(category).compareTo(value1) < 0)
                   result.put(category, client);
           }));    

    return result;

}

As the method's name sugggests, I want to find a map Map <String, Client>, containing Client with most purchases (as value) in specified category (as key) in each product's category

shopping is basically a map: Map<Client, Map<Product,Integer>>,


Not sure, if that's even possible? Collectors.collectingAndThen maybe could be useful?

Upvotes: 4

Views: 524

Answers (3)

Arthur Gazizov
Arthur Gazizov

Reputation: 132

You can use StreamEx library, and do smth like this

public static Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping) {
        return EntryStream.of(shopping)
                .flatMapKeyValue(((client, productQuantityMap) ->
                        EntryStream.of(productQuantityMap)
                                .mapToValue((p, q) -> p.getPrice().multiply(BigDecimal.valueOf(q)))
                                .mapKeys(Product::getCategory)
                                .map(e -> new ClientCategorySpend(client, e.getKey(), e.getValue())))
                )
                .groupingBy(
                        ClientCategorySpend::getCategory,
                        Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(ClientCategorySpend::getSpend)),
                                t -> t.get().getClient())
                );
    }

Upvotes: 1

Tomasz Gawel
Tomasz Gawel

Reputation: 8520

This should do ;)

public Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping) {

    return shopping
            .entrySet()
            .stream()
            .map(entry -> Pair.of(
                    entry.getKey(),
                    entry.getValue()
                            .entrySet()
                            .stream()
                            .map(e -> Pair.of(
                                    e.getKey().getCategory(),
                                    e.getKey().getPrice().multiply(
                                            BigDecimal.valueOf(e.getValue()))))
                            .collect(Collectors.toMap(
                                    Pair::getKey,
                                    Pair::getValue,
                                    BigDecimal::add))))

            // Here we have: Stream<Pair<Client, Map<String, BigDecimal>>>
            // e.g.: per each Client we have a map { category -> purchase value }

            .flatMap(item -> item.getValue()
                    .entrySet()
                    .stream()
                    .map(e -> Pair.of(
                            e.getKey(), Pair.of(item.getKey(), e.getValue()))))

            // Here: Stream<Pair<String, Pair<Client, BigDecimal>>>
            // e.g.: entries stream { category, { client, purchase value } }
            // where there are category duplicates, so we must select those  
            // with highest purchase value for each category.

            .collect(Collectors.toMap(
                    Pair::getKey,
                    Pair::getValue,
                    (o1, o2) -> o2.getValue().compareTo(o1.getValue()) > 0 ?
                            o2 : o1))

            // Now we have: Map<String, Pair<Client, BigDecimal>>,
            // e.g.: { category -> { client, purchase value } }
            // so just get rid of unnecessary purchase value...

            .entrySet()
            .stream()
            .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> e.getValue().getKey()));

}

Pair is org.apache.commons.lang3.tuple.Pair. If you do not want to use Appache Commons library you may use java.util.AbstractMap.SimpleEntry instead.

Upvotes: 0

daphshez
daphshez

Reputation: 9648

You were pretty much doomed the moment you grouped by client. The top level Collectors.groupingBy must use the category as the grouping by key.

To do that you would flatMap before collecting, so you get a flat stream of client + category + spend elements.

Here's one way to do it. I'll first define a POJO for the elements of the flattened stream:

  static class ClientCategorySpend
  {
      private final Client client;
      private final String category;
      private final BigDecimal spend;

      public ClientCategorySpend(Client client, String category, BigDecimal spend)
      {
          this.client = client;
          this.category = category;
          this.spend = spend;
      }

      public String getCategory()
      {
          return category;
      }

      public Client getClient()
      {
          return client;
      }

      public BigDecimal getSpend()
      {
          return spend;
      }
  }

And now the function:

public static Map<String, Client> clientsWithMostPurchasesInEachCategory(Map<Client, Map<Product, Integer>> shopping)
{
     // <1>
     Collector<? super ClientCategorySpend, ?, BigDecimal> sumOfSpendByClient = Collectors.mapping(ClientCategorySpend::getSpend,
             Collectors.reducing(BigDecimal.ZERO, BigDecimal::add));


     // <2>
     Collector<? super ClientCategorySpend, ?, Map<Client, BigDecimal>> clientSpendByCategory = Collectors.groupingBy(
             ClientCategorySpend::getClient,
             sumOfSpendByClient
     );

     // <3>
     Collector<? super ClientCategorySpend, ?, Client> maxSpendingClientByCategory = Collectors.collectingAndThen(
             clientSpendByCategory,
             map -> map.entrySet().stream()
                     .max(Comparator.comparing(Map.Entry::getValue))
                     .map(Map.Entry::getKey).get()
     );

     return shopping.entrySet().stream()
            // <4>
             .flatMap(
                     entry -> entry.getValue().entrySet().stream().map(
                             entry2 -> new ClientCategorySpend(entry.getKey(),
                                     entry2.getKey().category,
                                     entry2.getKey().price.multiply(BigDecimal.valueOf(entry2.getValue())))
                     )
             ).collect(Collectors.groupingBy(ClientCategorySpend::getCategory, maxSpendingClientByCategory));
}

Once I have a stream of ClientCategorySpend (4), I group it by category. I use the clientSpendByCategory collector (2) to create a map between the client and the total spend in the category. This in turn depends on sumToSpendByClient (1) which is basically a reducer that sums up the spends. You then get to use collectingAndThen as you suggested, reducing each Map<Client, BigDecimal> to a single client using max.

Upvotes: 0

Related Questions