7

I know there a similar questions asked in the forum but none of them seem to be addressing my problem fully. Now I'm very new to Java 8, so please bear with me. I have a list of Products, for example:

Input:
name    category    type    cost
prod1       cat2     t1      100.23
prod2       cat1     t2      50.23
prod1       cat1     t3      200.23
prod3       cat2     t1      150.23
prod1       cat2     t1      100.23


Output:
Single line (name, category, type) summing the cost and count of products.




Product {
    public String name;
    public String category;
    public String type;
    public int id;
    public double cost;

}

I need to group this by name, category and type and produce a single result that summarizes this data and produces the total cost and count of each product. Most examples show grouping by two fields and aggregating using single criteria.

Following suggestions on the forumn, i came up with this for groupings:

    public class ObjectKeys {

    ArrayList<Object> keys;

    public ObjectKeys(Object...searchKeys) {

         keys = new ArrayList<Object>();

            for (int i = 0; i < searchKeys.length; i++) {
                keys.add( searchKeys[i] );
            }
    }

}

Then used it as follows:

Map<String, Map<String, Map<String, List<Product>>>> productsByNameCategoryType =
    products.stream().collect(groupingBy(new ObjectKeys(l.name(), l.category(),l.type())))

But how do I chain count and sum to the the above code? Especially for group by with more than 2 fields. Is there a better way to do this?

Like I mentioned my Java8 is not that good, please help.

Stefan Zobel
  • 3,182
  • 7
  • 28
  • 38
Vuzi
  • 185
  • 2
  • 4
  • 13
  • Maybe you need an `Item` type to saving a `Product` and a `cost`. Then you can complete your work easily. – holi-java May 20 '17 at 18:18
  • @holi-java would you mind maybe expanding on your thoughts for me a little – Vuzi May 20 '17 at 18:21
  • I'm sorry I'm on phone. You can sum items group by products if you introduce an `Item` class and remove `cost` from `Priduct` as I mentioned. – holi-java May 20 '17 at 18:27

2 Answers2

7

Precondition

class Product {
    public String name;
    public String category;
    public String type;
    public int id; 
    //todo:implement equals(), toString() and hashCode()
 }

class Item{
   public Product product;
   public double cost;
}

Summarizing way

you can summarizing items grouping by product by using Collectors#groupingBy & Collectors#summarizingDouble.

List<Item> items = ...; 
Map<Product, DoubleSummaryStatistics> stat = items.stream().collect(groupingBy(
            it -> it.product,
            Collectors.summarizingDouble(it -> it.cost)
));

// get some product summarizing
long count = stat.get(product).getCount();
double sum = stat.get(product).getSum();

//list all product summarizing
stat.entrySet().forEach(it ->
  System.out.println(String.format("%s - count: %d, total cost: %.2f"
        , it.getKey(),it.getValue().getCount(), it.getValue().getSum()));
);

Merges Items with Same Product

First, you need adding a qty field in Item class:

class Item{
   public int qty;
   //other fields will be omitted

   public Item add(Item that) {
        if (!Objects.equals(this.product, that.product)) {
            throw new IllegalArgumentException("Can't be added items"
                     +" with diff products!");
        }
        return from(product, this.cost + that.cost, this.qty + that.qty);
    }

    private static Item from(Product product, double cost, int qty) {
        Item it = new Item();
        it.product = product;
        it.cost = cost;
        it.qty = qty;
        return it;
    }

}

then you can using Collectors#toMap to merges items with same product:

Collection<Item> summarized = items.stream().collect(Collectors.toMap(
        it -> it.product,
        Function.identity(),
        Item::add
)).values();

Finally

you can see both ways doing the same thing, but the second approach is easier to operates on a stream. and the tests of two ways I having checked in github, you can click and see more details: summarizing items & merge items ways.

holi-java
  • 29,655
  • 7
  • 72
  • 83
  • Thanks holi-java will test it just now – Vuzi May 20 '17 at 19:08
  • thanks for your help, one question though when grouping as your code here: (Collectors.groupingBy(it -> it.product, instead of grouping by one product shouldn't I be grouping by name,type and category? – Vuzi May 21 '17 at 02:53
  • @Vuzi Hi, I already mentioned in comments that two ways can does, but packaging data into objects will make your work *easier* and *pleasure*. and you proposed in comment you need a `ProductSummary` you can add a `qty` in `Item` and just grouping items to items by products.on the other hand, you expect the result type is `List` not is `Map`. – holi-java May 21 '17 at 03:09
  • @Vuzi I'm sorry I answer your question on my phone last night and it's inconvenient, and I providing two approaches for your question now. you can trying and thinking: "Why did I suggest you packing fields into objects?". – holi-java May 21 '17 at 07:07
4

Here’s the quick and dirty solution:

    Map<String, String> productsByNameCategoryType = products.stream()
            .collect(Collectors.groupingBy(p 
                            -> p.getName() + '-' + p.getCategory() + '-' + p.getType(),
                    Collectors.collectingAndThen(
                            Collectors.summarizingDouble(Product::getCost),
                            dss -> String.format("%7.2f%3d", 
                                                 dss.getSum(), dss.getCount()))));

You will probably want to build your own classes for both the keys and the values of the result map. In any case, with your data and the above code the map contains four entries:

prod1-cat1-t3:  200,23  1
prod1-cat2-t1:  200,46  2
prod3-cat2-t1:  150,23  1
prod2-cat1-t2:   50,23  1

The sums are printed with comma as decimal point because my computer has Danish locale (you can pass a locale to String.format() to control the locale if you want).

Your friend is the combination of Collectors.collectingAndThen() and Collectors.summarizingDouble(). I took that from this answer.

Community
  • 1
  • 1
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • This works as well, thanks especially with the Collectors.collectingAndThen() and Collectors.summarizingDouble(), they are very handy indeed. Though the methood is "dirty", it's an eye opener – Vuzi May 22 '17 at 10:55