Home > database >  How to merge two Maps based on values with Java 8 streams?
How to merge two Maps based on values with Java 8 streams?

Time:03-11

I have a Collection of Maps containing inventory information:

 0 
  "subtype" -> "DAIRY"
  "itemNumber" -> "EU999"
  "quantity" -> "60"
 1 
  "subtype" -> "DAIRY"
  "itemNumber" -> "EU999"
  "quantity" -> "1000"
 2 
  "subtype" -> "FRESH"
  "itemNumber" -> "EU999"
  "quantity" -> "800"
 3
  "subtype" -> "FRESH"
  "itemNumber" -> "EU100"
  "quantity" -> "100"

I need to condense this list based on the itemNumber, while summing the quantity and retaining unique subtypes in a comma separated string. Meaning, new Maps would look like this:

 0 
  "subtype" -> "DAIRY, FRESH"
  "itemNumber" -> "EU999"
  "quantity" -> "1860"
 1 
  "subtype" -> "FRESH"
  "itemNumber" -> "EU100"
  "quantity" -> "100"

I've tried a variations of streams, collectors, groupby etc., and I'm lost.

This is what I have so far:

public Collection<Map> mergeInventoryPerItemNumber(Collection<Map> InventoryMap){
        Map condensedInventory = null;
        InventoryMap.stream()
                .collect(groupingBy(inv -> new ImmutablePair<>(inv.get("itemNumber"), inv.get("subtype")))), collectingAndThen(toList(), list -> {
            long count = list.stream()
                    .map(list.get(Integer.parseInt("quantity")))
                    .collect(counting());
            String itemNumbers = list.stream()
                    .map(list.get("subtype"))
                    .collect(joining(" , "));
            condensedInventory.put("quantity", count);
            condensedInventory.put("subtype", itemNumbers);

            return condensedInventory;
        });

CodePudding user response:

It may be possible to do this with a single sweep, but here I have solved it with two passes: one to group like items together, and another over the items in each group to build a representative item (which seems similar in spirit to your code, where you were also attempting to stream elements from groups).

   
    public static Collection<Map<String, String>> 
            mergeInventoryPerItemNumber(Collection<Map<String, String>> m){

        return m.stream()
                // returns a map of itemNumber -> list of products with that number
                .collect(Collectors.groupingBy(o -> o.get("itemNumber")))
                // for each item number, builds new representative product
                .entrySet().stream().map(e -> Map.of(
                    "itemNumber", e.getKey(), 
                    // ... merging non-duplicate subtypes
                    "subtype", e.getValue().stream()
                        .map(v -> v.get("subtype"))
                        .distinct() // avoid duplicates
                        .collect(Collectors.joining(", ")), 
                    // ... adding up quantities
                    "quantity", "" e.getValue().stream()
                        .map(v -> Integer.parseInt(v.get("quantity")))
                        .reduce(0, Integer::sum)))
                .collect(Collectors.toList());
    }

    public static void main(String ... args) {
        Collection<Map<String, String>> c = mkMap();
        dump(c);
        dump(mergeInventoryPerItemNumber(c));
    }

    public static Collection<Map<String, String>> mkMap() {
        return List.of(
            Map.of("subtype", "DAIRY", "itemNumber", "EU999", "quantity", "60"),
            Map.of("subtype", "DAIRY", "itemNumber", "EU999", "quantity", "1000"),
            Map.of("subtype", "FRESH", "itemNumber", "EU999", "quantity", "800"),
            Map.of("subtype", "FRESH", "itemNumber", "EU100", "quantity", "100"));
    }

    public static void dump(Collection<Map<String, String>> col) {
        int i = 0;
        for (Map<String, String> m : col) {
            System.out.println(i  );
            for (Map.Entry e : m.entrySet()) {
                System.out.println("\t"   e.getKey()   " -> "   e.getValue());
            }
        }
    }

CodePudding user response:

You are misusing a Map here. Every map contains the same keys ("subtype", "itemNumber", "quantity"). And they are treated almost like object properties in your code. They are expected to be present in every map and each of them expected to have a specific range of values, although are stored as strings to according to your example.

Side-note: avoid using row types (like Map without generic information in angle brackets <>), in such a case all elements inside a collection will be treated as Objects.

Item clearly has to be defined as a class. By storing these data inside a map, you're loosing a possibility to define an appropriate data type for each property, and also you're not able to define behavior to manipulate with these properties (for more elaborate explanation take a look at this answer).

public class Item {
    private final String itemNumber;
    private Set<Subtype> subtypes;
    private long quantity;

    public Item combine(Item other) {
        Set<Subtype> combinedSubtypes = new HashSet<>(subtypes);
        combinedSubtypes.addAll(other.subtypes);

        return new Item(this.itemNumber,
                        combinedSubtypes,
                        this.quantity   other.quantity);
    }

    //   constructor, getters, hashCode/equals, toString
}

Method combine represents the logic for merging two items together. By placing it inside this class you could easily reuse and change.

The best choice for the type of the subtype field is an enum. Because it'll allow to avoid mistakes caused by misspelled string values and also enums have an extensive language support (switch expressions and statements, special data structures designed especially for enums, enum could be used with annotations).

This custom enum can look like this.

public enum Subtype {DAIRY, FRESH}

With all these changes, the code inside the mergeInventoryPerItemNumber() becomes concise and easier to comprehend. Collectors.groupingBy() is used to create a map by grouping items with the same itemNumber. A downstream collector Collectors.reducing() is used to combine items grouped under the same key to a single object.

Note that Collectors.reducing() produces an Optional result. Therefore, filter(Optional::isPresent) is used as a precaution to make sure that the result exists and subsequent operation map(Optional::get) extracts the item from the optional object.

public static Collection<Item> mergeInventoryPerItemNumber(Collection<Item> inventory) {
    return inventory.stream()
            .collect(Collectors.groupingBy(Item::getItemNumber,
                            Collectors.reducing(Item::combine)))
            .values().stream()
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toList());
}

main()

public static void main(String[] args) {
    List<Item> inventory =
            List.of(new Item("EU999", Set.of(Subtype.DAIRY), 60),
                    new Item("EU999", Set.of(Subtype.DAIRY), 1000),
                    new Item("EU999", Set.of(Subtype.FRESH), 800),
                    new Item("EU100", Set.of(Subtype.FRESH), 100));

    Collection<Item> combinedItems = mergeInventoryPerItemNumber(inventory);

    combinedItems.forEach(System.out::println);
}

Output

Item{itemNumber='EU100', subtypes=[FRESH], quantity=100}
Item{itemNumber='EU999', subtypes=[FRESH, DAIRY], quantity=1860}

CodePudding user response:

Here is one approach.

  • first iterate thru the list of maps.
  • for each map, process the keys as required
    • special keys are itemNumber and quantity
    • itemNumber is the joining element for all the values.
    • quantity is the value that must be treated as an integer
    • the others are strings and are treated as such (for all other values, if the value already exists in the string of concatenated values, then it is not added again)

Some data

List<Map<String, String>> mapList = List.of(
        Map.of("subtype", "DAIRY", "itemNumber", "EU999",
                "quantity", "60"),
        Map.of("subtype", "DAIRY", "itemNumber", "EU999",
                "quantity", "1000"),
        Map.of("subtype", "FRESH", "itemNumber", "EU999",
                "quantity", "800"),
        Map.of("subtype", "FRESH", "itemNumber", "EU100",
                "quantity", "100"));

The building process

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

for (Map<String, String> m : mapList) {
    result.compute(m.get("itemNumber"), (k, v) -> {
        for (Entry<String, String> e : m.entrySet()) {
            String key = e.getKey();
            String value = e.getValue();
            if (v == null) {
                v = new HashMap<String, String>();
                v.put(key, value);
            } else {
                if (key.equals("quantity")) {
                    v.compute(key,
                            (kk, vv) -> vv == null ? value :
                                    Integer.toString(Integer
                                            .valueOf(vv)
                                              Integer.valueOf(
                                                    value)));
                } else {
                    v.compute(key, (kk, vv) -> vv == null ?
                            value : (vv.contains(value) ? vv :
                                    vv   ", "   value));
                }
            }
        }
        return v;
    });
}

List<Map<String,String>> list = new ArrayList<>(result.values());
        
for (int i = 0; i < list.size(); i  ) {
  System.out.println(i   " "   list.get(i));
}

prints

0 {itemNumber=EU100, quantity=100, subtype=FRESH}
1 {itemNumber=EU999, quantity=1860, subtype=DAIRY, FRESH}

Note that the map of maps may be more useful that a list of maps. For example, you can retrieve the map for the itemNumber by simply specifying the desired key.

System.out.println(result.get("EU999"));

prints

{itemNumber=EU999, quantity=1860, subtype=DAIRY, FRESH}

  • Related