Java 8 Stream List<Foo> to Map<Date, Map<String,Long>> with conditional groupingBy

  • A+
Category:Languages

Following class:

public class Foo {     private Date date;     private String name;     private Long number; } 

I now have a List<Foo> which I want to convert to Map<Date, Map<String,Long>> (Long should be a sum of numbers). What makes this hard is that I want exactly 26 entries in the inner map, where the 26th is called "Others" which sums up everything that has a number lower than the other 25.

I came up with following code:

data.stream().collect(Collectors.groupingBy(e -> e.getDate(), Collectors.groupingBy(e -> {     if (/*get current size of inner map*/>= 25) {         return e.getName();     } else {         return "Other";     }  }, Collectors.summingLong(e -> e.getNumber())))); 

As you can see, I have no idea how to check the number of elements which are already in the inner map. How can I get the current size of the inner map or is there another way to achieve what I want?

My Java 7 code:

Map<Date, Map<String, Long>> result = new LinkedHashMap<Date, Map<String, Long>>(); for (Foo fr : data) {     if (result.get(fr.getDate()) == null) {         result.put(fr.getDate(), new LinkedHashMap<String, Long>());     }     if (result.get(fr.getDate()) != null) {         if (result.get(fr.getDate()).size() >= 25) {             if (result.get(fr.getDate()).get("Other") == null) {                 result.get(fr.getDate()).put("Other", 0l);             }             if (result.get(fr.getDate()).get("Other") != null) {                 long numbers= result.get(fr.getDate()).get("Other");                 result.get(fr.getDate()).replace("Other", numbers+ fr.getNumbers());             }         } else {             result.get(fr.getDate()).put(fr.getName(), fr.getNumbers());         }     } } 

Edit:

The map should help me to realize a table like this:

Java 8 Stream List<Foo> to Map<Date, Map<String,Long>> with conditional groupingBy

But I need to sum the "Others" first.


If you need any more infos feel free to ask

 


I don’t think that this operation will benefit from using the Stream API. Still, you can improve the operation with Java 8 features:

Map<Date, Map<String, Long>> result = new LinkedHashMap<>(); for(Foo fr : data) {     Map<String, Long> inner       = result.computeIfAbsent(fr.getDate(), date -> new LinkedHashMap<>());     inner.merge(inner.size()>=25?"Other":fr.getAirlineName(), fr.getNumbers(), Long::sum); } 

This code assumes that the airline names are already unique for each date. Otherwise, you would have to extend the code to

Map<Date, Map<String, Long>> result = new LinkedHashMap<>(); for(Foo fr : data) {     Map<String, Long> inner       = result.computeIfAbsent(fr.getDate(), date -> new LinkedHashMap<>());     inner.merge(inner.size() >= 25 && !inner.containsKey(fr.getAirlineName())?       "Other": fr.getAirlineName(), fr.getNumbers(), Long::sum); } 

to accumulate the values for the airline correctly.


For completeness, here is how to implement it as a stream operation.

Since the custom collector has some complexity, it’s worth writing it as reusable code:

public static <T,K,V> Collector<T,?,Map<K,V>> toMapWithLimit(     Function<? super T, ? extends K> key, Function<? super T, ? extends V> value,     int limit, K fallBack, BinaryOperator<V> merger) {      return Collector.of(LinkedHashMap::new, (map, t) ->             mergeWithLimit(map, key.apply(t), value.apply(t), limit, fallBack, merger),             (map1,map2) -> {                 if(map1.isEmpty()) return map2;                 if(map1.size()+map2.size() < limit)                     map2.forEach((k,v) -> map1.merge(k, v, merger));                 else                     map2.forEach((k,v) ->                         mergeWithLimit(map1, k, v, limit, fallBack, merger));                 return map1;             }); } private static <T,K,V> void mergeWithLimit(Map<K,V> map, K key, V value,     int limit, K fallBack, BinaryOperator<V> merger) {     map.merge(map.size() >= limit && !map.containsKey(key)? fallBack: key, value, merger); } 

This is like Collectors.toMap, but supporting a limit and a fallback key for additional entries. You may recognize the Map.merge call, similar to the loop solution as the crucial element.

Then, you may use the collector as

Map<Date, Map<String, Long>> result = data.stream().collect(     Collectors.groupingBy(Foo::getDate, LinkedHashMap::new,         toMapWithLimit(Foo::getAirlineName, Foo::getNumbers, 25, "Other", Long::sum))); 

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: