2

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.

3
  • Group by again in the downstream and use a reducing collector as the downstream of the second group by. Commented Oct 21, 2021 at 6:15
  • thanks, any ideas on how to join using the BinaryOperator on reducing ? Commented Oct 21, 2021 at 7:17
  • I do not want one list, I still need the map returned Commented Oct 21, 2021 at 7:37

2 Answers 2

2

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 ) ...
Sign up to request clarification or add additional context in comments.

1 Comment

thanks for sharing and explaining in detail, I am still not able to figure out the Note 2 collectingAndThen example that you have mentioned. how would that be implemented ?
2

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.

Comments

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.