user641887
user641887

Reputation: 1576

Java Streams, GroupBy and sum quantities similar items in the list?

I have a class something like this

public class Item {
    private String sku;
    private int quantity;
    private int amount;
    private String txnId;
}

I have a list of these items and I want to fist group the items by txnId and then if the SKU of the 2 items are same I need add the quantity and the amount fields?

Item["productA",1,100,"ABC"] 
Item["productB",2,200,"ABC"]
Item["productA",3,200,"ABC"]

Item["productB",2,200,"PQR"]
Item["productB",2,200,"PQR"]
Item["productA",2,200,"PQR"]

at the end I should be having

MAP["ABC",List<Item["productA",4,300,"ABC"],Item["productB",2,200,"ABC"],
   "PQR",List<Item["productB",4,400,"PQR"],Item["productA",2,200,"PQR"]

I am able to group by the txnID

list.stream().collect(Collectors.groupingBy(txnID) but I am not sure how I should check for the same SKU and if exists add the amount and quantity and get the map output.

Upvotes: 2

Views: 2428

Answers (2)

adarsh
adarsh

Reputation: 1503

toMap offers a good alternative to merge values in such cases.

Here's what another solution looks like:

var result = itemList.stream().collect(
                    groupingBy(Item::getTxnId,
                    collectingAndThen(toMap(Item::getSku, i -> i, Item::sum), 
                                      Map::values)));

// where Item::sum is a reference to a static method in class Item
static Item sum(Item i1, Item i2) {
    return  new Item(i1.getSku(),
            i1.getQuantity() + i2.getQuantity(),
            i1.getAmount() + i2.getAmount(),
            i1.getTxnId());
}

Unlike groupingBy (which takes a Function and then a Collector), collectingAndThen takes a collector (here, it's toMap) and then takes a Function (here, Map::values) that works on the result of the collector.

Upvotes: 2

user16320675
user16320675

Reputation: 135

There is a 2 argument groupingBy method - the second argument is a Collector that is applied to the value (a List) associated to each key.

Let's do it step-by-step.

Assuming import static java.util.stream.Collectors.*;
and that we have the list:

List<Item> list = List.of(
  new Item("A", 1, 100, "ABC"),
  new Item("B", 2, 200, "ABC"),
  new Item("A", 3, 300, "ABC"),
  new Item("B", 2, 200, "PQR"),
  new Item("B", 2, 200, "PQR"),
  new Item("A", 2, 200, "PQR"));

  • First step: group by txnId as already in question:
list.stream().collect(groupingBy(Item::txnId))

this will result in a Map<String,List<Item>>:

{ PQR=[Item[sku=B, quantity=2, amount=200, txnId=PQR], 
       Item[sku=B, quantity=2, amount=200, txnId=PQR], 
       Item[sku=A, quantity=2, amount=200, txnId=PQR]
      ], 
  ABC=[Item[sku=A, quantity=1, amount=100, txnId=ABC], 
       Item[sku=B, quantity=2, amount=200, txnId=ABC], 
       Item[sku=A, quantity=3, amount=300, txnId=ABC]
      ]
}

  • Second step: group each internal list by sku:
list
.stream()
.collect(groupingBy(Item::txnId, 
                    groupingBy(Item::sku)))

resulting in a Map<String,Map<String,List<Item>>:

{ PQR={ A=[ Item[sku=A, quantity=2, amount=200, txnId=PQR] ], 
        B=[ Item[sku=B, quantity=2, amount=200, txnId=PQR], 
            Item[sku=B, quantity=2, amount=200, txnId=PQR] ]
      }, 
  ABC={ A=[ Item[sku=A, quantity=1, amount=100, txnId=ABC], 
            Item[sku=A, quantity=3, amount=300, txnId=ABC] ], 
        B=[ Item[sku=B, quantity=2, amount=200, txnId=ABC] ]
      }
}

  • Third step reduce the internal lists, summing the values:

I will assume we have a sum method to sum 2 Items returning a new one, very simplified:

Item {
    // fields
    // getters and setters

    public static Item sum(Item item1, Item item2) {
        return new Item(item2.sku, item1.quantity+item2.qantity, item1.amount+item2.amount, item2.txnId);
    }
}

this could also be defined as a 1-arg non-static method

Now we can use reducing to add up the elements of the internal list:

list
.stream()
.collect(groupingBy(Item::txnId, 
                    groupingBy(Item::sku,
                               reducing(new Item(null, 0, 0, null),
                                        Item::sum))))

finally resulting in:

{ PQR={ A=Item[sku=A, quantity=2, amount=200, txnId=PQR], 
        B=Item[sku=B, quantity=4, amount=400, txnId=PQR]
      }, 
  ABC={ A=Item[sku=A, quantity=4, amount=400, txnId=ABC], 
        B=Item[sku=B, quantity=2, amount=200, txnId=ABC]
      }
}


Note1: A Lambda expression may be used instead of the sum method for reducing:

....reducing( new Item(null, 0, 0, null),
              (i1,i2) -> new Item(i2.sku,
                                  i1.quantity+i2.quantity,
                                  i1.amount+i2.amount,
                                  i2.txnId) )

Note2: To convert the inner Maps to a List use collectingAndThen with Map::values around the second groupingBy:

... collectingAndThen( groupingBy(Item::sku, ...), Map:values ) ...

Upvotes: 2

Related Questions