2

I have a list of Maps and want to group and sum on certain columns.

List of Maps:

double min=100;
double max=999;
List<Map<String,Object>> positions = new ArrayList<Map<String,Object>>();
for (int i=0; i<10; i++) {
    Map<String,Object> positionRow = new HashMap<String,Object>();
    positionRow.put("id", i+1);
    if ((i+1)%2 == 0)
        positionRow.put("Mod", "Even");
    else
        positionRow.put("Mod", "Odd");
    Random r = new Random();
    double randomValue = min + (max - min) * r.nextDouble();
    positionRow.put("price", randomValue);
    positionRow.put("bigger", (randomValue > 300)?"Y":"N");
    positions.add(positionRow);
}

I am trying to use Java 8 stream with Collectors.groupingBy and Collectors.summingDouble to get results as Map<String,Map<String,Object>>. So far, I can get Map<String,List<Map<String,Object>>> by using Java 8 stream with Collectors.groupingBy.

Group by "Mod" and "bigger":

Map<String, List<Map<String, Object>>> grouped = positions.stream()
        .collect(
                Collectors.groupingBy(m -> m.get("Mod").toString()
                        + "<|>" + m.get("bigger").toString()));

Group by "Mod" and "bigger" results:

{Even<|>Y=[{Mod=Even, price=872.729803251601, bigger=Y, id=2}, {Mod=Even, price=353.6589309614915, bigger=Y, id=4}, {Mod=Even, price=981.8179976373482, bigger=Y, id=6}, {Mod=Even, price=942.3538966530067, bigger=Y, id=8}, {Mod=Even, price=919.0174189044218, bigger=Y, id=10}], Odd<|>Y=[{Mod=Odd, price=663.7589137270676, bigger=Y, id=1}, {Mod=Odd, price=894.1766799283644, bigger=Y, id=3}, {Mod=Odd, price=905.8003509608488, bigger=Y, id=5}, {Mod=Odd, price=758.3085768816934, bigger=Y, id=7}, {Mod=Odd, price=531.1035747782346, bigger=Y, id=9}]}

I am expecting Group by "Mod" and "bigger" and sum on "id" and "price" results(Map<String,Map<String,Object>>):

{Even<|>Y={sum_price=4069.578047, sum_id=30}, Odd<|>Y={sum_price=3753.148096,sum_id=25}}

Any suggestions? Thank you,

Welly
  • 43
  • 1
  • 9

2 Answers2

2

Map<String, Object> is not a very good type for maintaining objects. That’s exactly why classes and fields having a name and declared type were invented. The required code for the collector reflects this:

Map<String, Map<String, Object>> grouped = positions.stream()
.collect(Collectors.groupingBy(m -> m.get("Mod")+"<|>"+m.get("bigger"),
    Collector.of(HashMap::new, (m,p)-> {
        m.merge("sum_price", p.get("price"), (a,b)->((Double)a)+((Double)b));
        m.merge("sum_id", p.get("id"), (a,b)->((Integer)a)+((Integer)b));
    }, (m1,m2)-> {
        m1.merge("sum_price", m2.get("sum_price"), (a,b)->((Double)a)+((Double)b));
        m1.merge("sum_id", m2.get("sum_id"), (a,b)->((Integer)a)+((Integer)b));
        return m1;
    })));
Holger
  • 285,553
  • 42
  • 434
  • 765
  • It works. But what does java.util.function.BinaryOperator of Collector.of do? I tried without any logic (m1,m2) -> {return m1;} and it still works. – Welly Jul 09 '15 at 20:40
  • 1
    The combiner is usually only used with *parallel* streams. Then, each thread will use the first argument (supplier) to create a local container and the second function to collect items into the local container and once two threads have processed their part of the stream, they’ll use the third function for merging two containers (here the container type is a `Map`). So here it’s not used but you should decide for either, still provide a working function or provide a throwing function to detect the absent support. Don’t provide a function that will silently break once it becomes used… – Holger Jul 10 '15 at 08:43
1

You want to collect two independent sums into single result. To do this you can use a "pairing" Collector which I wrote in this answer:

Collector<Map<String, Object>, ?, Map<String, Object>> collector = 
        pairing(
                Collectors.summingDouble(m -> (Double)m.get("price")), 
                Collectors.summingInt(m -> (Integer)m.get("id")), 
        (sumPrice, sumId) -> {
            Map<String, Object> res = new HashMap<>();
            res.put("sum_price", sumPrice);
            res.put("sum_id", sumId);
            return res;
        });
Map<String, Map<String, Object>> grouped = positions.stream()
        .collect(
                Collectors.groupingBy(m -> m.get("Mod").toString()
                        + "<|>" + m.get("bigger").toString(), collector));

Note that this collector is readily available in my StreamEx library. Using other features of my library the solution can be even shorter:

Collector<Map<String, Object>, ?, Map<String, Object>> collector = 
        MoreCollectors.pairing(
                Collectors.summingDouble(m -> (Double)m.get("price")), 
                Collectors.summingInt(m -> (Integer)m.get("id")), 
        (sumPrice, sumId) -> EntryStream.<String, Object>of
                ("sum_price", sumPrice, "sum_id", sumId).toMap());
Map<String, Map<String, Object>> grouped = StreamEx.of(positions)
        .groupingBy(m -> m.get("Mod") + "<|>" + m.get("bigger"), collector);
Community
  • 1
  • 1
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334