7

I am using Java 8 streams to group a list of entries by a certain key and then sorting the groups by date. What I would like to do in addition is to "collapse" any two entries within a group that have the same date and sum them up. I have a class like this (stripped down for example purposes)

class Thing {
    private String key;
    private Date activityDate;
    private float value;
    ...
}

Then I'm grouping them like so:

Map<String, List<Thing>> thingsByKey = thingList.stream().collect(
                Collectors.groupingBy(
                        Thing::getKey,
                        TreeMap::new,
                        Collectors.mapping(Function.identity(), toSortedList())
                ));

private static Collector<Thing,?,List<Thing>> toSortedList() {
        return Collectors.collectingAndThen(toList(),
                l -> l.stream().sorted(Comparator.comparing(Thing::getActivityDate)).collect(toList()));
    }

What I would like to do is, if any two Thing entries have the exact same date, sum up the values for those and collapse them down such that,

Thing1 Date=1/1/2017 Value=10

Thing2 Date=1/1/2017 Value=20

Turns into 30 for 1/1/2017.

What's the best way to accomplish something like that?

cloudwalker
  • 2,346
  • 1
  • 31
  • 69
  • 5
    I know this is unrelated to your question, but I would strongly suggest avoiding the legacy `java.util.Date` if you possibly can. You should instead look in the `java.time` package for the class most appropriate for your use case. – Joe C Jun 25 '17 at 15:21
  • 3
    Use toMap(), not groupingBy(). https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function-java.util.function.BinaryOperator-java.util.function.Supplier- – JB Nizet Jun 25 '17 at 15:22
  • @JoeC In my actual code I'm using the java.time package. This was just a simplified example I threw together to make it easy to read. – cloudwalker Jun 25 '17 at 15:56

2 Answers2

5

I have slightly change your Thing class to use LocalData and added a very simple toString:

@Override
public String toString() {
   return " value = " + value;
}

If I understood correctly, than this is what you need:

Map<String, TreeMap<LocalDate, Thing>> result = Arrays
            .asList(new Thing("a", LocalDate.now().minusDays(1), 12f), new Thing("a", LocalDate.now(), 12f), new Thing("a", LocalDate.now(), 13f))
            .stream()
            .collect(Collectors.groupingBy(Thing::getKey,
                    Collectors.toMap(Thing::getActivityDate, Function.identity(),
                            (Thing left, Thing right) -> new Thing(left.getKey(), left.getActivityDate(), left.getValue() + right.getValue()),
                            TreeMap::new)));


 System.out.println(result); // {a={2017-06-24= value = 12.0, 2017-06-25= value = 25.0}}
Eugene
  • 117,005
  • 15
  • 201
  • 306
2

This can be accomplished using the toMap collector:

Map<Date, Thing> thingsByDate = things.stream().collect(Collectors.toMap(
    Thing::getActivityDate,
    Function.identity(),
    (thing1, thing2) -> new Thing(null, thing1.getActivityDate(), thing1.getValue()+thing2.getValue())
);

You may then do with this map as you wish.

Joe C
  • 15,324
  • 8
  • 38
  • 50
  • 3
    Add TreeMap::new as 4th argument, to have a sorted map. – JB Nizet Jun 25 '17 at 15:26
  • This doesn't compile. You should merge two `Float` values in your merge function, or use `Function.identity()` as the value mapper. Besides this, this is not what the OP is asking. He is asking to collapse 2 or more elements that have the same date *within each list*. I mean, OP doesn't want a `Map` as a result, but a `Map>` instead, where the key of the map is the `key` of the `Thing`. – fps Jun 25 '17 at 15:34
  • Ah, right. Fixed that first bit. As for the second bit, I reckon the OP should be able to figure out how to work it in. – Joe C Jun 25 '17 at 15:36