I have a Collection
of Map
s 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 Map
s 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 Object
s.
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
andquantity
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)
- special keys are
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}