DEV Community

yangbongsoo
yangbongsoo

Posted on

Grouping the Same Products in Java

First, one must understand the relationship between Product and Item. A Product can have multiple Items. Simply put, a Product is the product information, and an Item is the option information.

import java.util.List;

import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class Product {
    String productId;

    List<Item> items;

    @Value
    @Builder
    public static class Item {
        String itemId;

        long itemQuantity;
    }
}
Enter fullscreen mode Exit fullscreen mode

In the sample products, there are a total of three Products, two of which have the same productId. At the Item level, itemId 1 and 2 also exist as duplicates. Ultimately, the goal is to group by the same productId and the same itemId.

public class Main {
  public static void main(String[] args) {
    /**
     * [
     *   Product(
     *     productId=1,
     *     items=[
     *       Product.Item(itemId=1, itemQuantity=1),
     *       Product.Item(itemId=1, itemQuantity=1),
     *       Product.Item(itemId=2, itemQuantity=2)
     *     ]
     *   ),
     *   Product(
     *     productId=1,
     *     items=[
     *       Product.Item(itemId=3, itemQuantity=3),
     *       Product.Item(itemId=2, itemQuantity=2)
     *     ]
     *   ),
     *   Product(
     *     productId=2,
     *     items=[
     *       Product.Item(itemId=5, itemQuantity=5)
     *     ]
     *   )
     * ]
     */
     List<Product> products = getProducts();
  }

  private static List<Product> getProducts() {
    return List.of(
      Product.builder()
        .productId("1")
        .items(List.of(
          Item.builder()
            .itemId("1")
            .itemQuantity(1)
            .build(),
          Item.builder()
            .itemId("1")
            .itemQuantity(1)
            .build(),
          Item.builder()
            .itemId("2")
            .itemQuantity(2)
            .build()
        )
      )
      .build(),
      Product.builder()
        .productId("1")
        .items(List.of(
          Item.builder()
            .itemId("3")
            .itemQuantity(3)
            .build(),
          Item.builder()
            .itemId("2")
            .itemQuantity(2)
            .build()
        )
      )
      .build(),
      Product.builder()
        .productId("2")
        .items(List.of(
          Item.builder()
            .itemId("5")
            .itemQuantity(5)
            .build()
          )
        )
      .build()
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The ProductGroup class below holds the results of grouping by the same productId and the same itemId. It can have multiple ItemGroups per productId without duplicates, and the itemId within the ItemGroup also does not duplicate. Finally, the itemQuantity for the same itemId is summed up.

@Value
@Builder(access = AccessLevel.PRIVATE)
public class ProductGroup {
    String productId;

    List<ItemGroup> itemGroups;

    @Value
    @Builder(access = AccessLevel.PRIVATE)
    public static class ItemGroup {
        String itemId;

        long totalQuantity;
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's now look at the logic for grouping. The first thing to do is to group the Products by productId.

public static List<ProductGroup> grouping(List<Product> products) {
  return products.stream()
    // first process Map<String, List<Product>>
    .collect(Collectors.groupingBy(Product::getProductId, toList()))


    .entrySet() // Set<Map<K,V>.Entry<String, List<Product>>>
    .stream() // Stream<Map<K,V>.Entry<String, List<Product>>>

    // second process
    .map(ProductGroup::makeEachProductGroups) // Stream<ProductGroup>
    .toList();
}
Enter fullscreen mode Exit fullscreen mode

After performing the first process, it is stored in a Map as described in the comment below. Using this Map, a ProductGroup is created by utilizing entrySet().stream().

public static List<ProductGroup> grouping(List<Product> products) {
  return products.stream()

    /**
     * first process : Map<String, List<Product>> productListsById
     * {
     * 1=[
     *     Product(
     *       productId=1,
     *       items=[
     *         Product.Item(itemId=1, itemQuantity=1),
     *         Product.Item(itemId=1, itemQuantity=1),
     *         Product.Item(itemId=2, itemQuantity=2)
     *       ]
     *     ),
     *     Product(
     *       productId=1,
     *       items=[
     *         Product.Item(itemId=3, itemQuantity=3),
     *         Product.Item(itemId=2, itemQuantity=2)
     *       ]
     *     )
     *   ],
     * 2=[
     *     Product(
     *       productId=2,
     *       items=[Product.Item(itemId=5, itemQuantity=5)]
     *     )
     *   ]
     * }
     */
    .collect(Collectors.groupingBy(Product::getProductId, toList()))

    .entrySet() // Set<Map<K,V>.Entry<String, List<Product>>>
    .stream() // Stream<Map<K,V>.Entry<String, List<Product>>>

    // second process
    .map(ProductGroup::makeEachProductGroups) // Stream<ProductGroup>
    .toList();
Enter fullscreen mode Exit fullscreen mode

cf) If toUnmodifiableMap is used as shown below, a duplicateKeyException occurs if there are duplicate productIds.

Map<String, Product> productsById = products.stream()
.collect(Collectors.toUnmodifiableMap(Product::getProductId, Function.identity()));
Enter fullscreen mode Exit fullscreen mode

In the subsequent secondary task, a ProductGroup is created. Then, moving on to the ItemGroup stage (makeItemGroups), the Items within a Product are first turned into a Stream using flatMap(product -> product.getItems().stream()), and then grouped again by itemId.

Using the results, an ItemGroup is created, and the totalQuantity is calculated by summing the quantities of the grouped Items.

private static ProductGroup makeEachProductGroups(Entry<String, List<Product>> productListById) {
  return ProductGroup.builder()
    .productId(productListById.getKey())
    .itemGroups(makeItemGroups(productListById.getValue()))
    .build();
  }

private static List<ItemGroup> makeItemGroups(List<Product> products) {
  return products.stream() // Stream<Product>
    .flatMap(product -> product.getItems().stream()) // Stream<Product.Item>
    .collect(groupingBy(Item::getItemId, toList())) // Map<String, List<Product.Item>>
    .entrySet().stream() // Stream<Map<K,V>.Entry<String, List<Product.Item>>>
    .map(it ->
      ItemGroup.builder()
        .itemId(it.getKey())
        .totalQuantity(it.getValue().stream().mapToLong(Item::getItemQuantity).sum())
        .build()
    ) // Stream<ProductGroup.ItemGroup>
    .toList();
  }
Enter fullscreen mode Exit fullscreen mode

The final result obtained from the grouping is as follows.

    /**
     * second process : grouping
     * [
     *   ProductGroup(
     *     productId=1,
     *     itemGroups=[
     *       ProductGroup.ItemGroup(itemId=1, totalQuantity=2),
     *       ProductGroup.ItemGroup(itemId=2, totalQuantity=4),
     *       ProductGroup.ItemGroup(itemId=3, totalQuantity=3)
     *     ]
     *   ),
     *   ProductGroup(
     *     productId=2,
     *     itemGroups=[
     *       ProductGroup.ItemGroup(itemId=5, totalQuantity=5)
     *     ]
     *   )
     * ]
     */
Enter fullscreen mode Exit fullscreen mode

There's one important point to note in the makeItemGroups logic. After turning the Items within a Product into a Stream using flatMap(product -> product.getItems().stream()), it is essential to group them again by itemId.

It should not be done by grouping using the items of each product as follows.

private static List<ItemGroup> makeItemGroupsWrongWay(List<Product> products) {
  return products.stream()
    .flatMap(product -> // don't do that each product
      product.getItems().stream()
        .collect(groupingBy(Item::getItemId, toList()))
        .entrySet()
        .stream()
        .map(it ->
          ItemGroup.builder()
            .itemId(it.getKey())
            .totalQuantity(it.getValue().stream().mapToLong(Item::getItemQuantity).sum())
            .build()
        ))
        .toList();
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)