javadictionarycollectionsmultimapmulti-mapping

Invert a Map with redundant values to produce a multimap


Given a map such as this where we have a frequency count per day-of-week for a year:

Map.of(
    DayOfWeek.MONDAY , 52 ,
    DayOfWeek.TUESDAY , 52 ,
    DayOfWeek.WEDNESDAY, 53 ,
    DayOfWeek.THURSDAY , 53 ,
    DayOfWeek.FRIDAY , 52 ,
    DayOfWeek.SATURDAY , 52 ,
    DayOfWeek.SUNDAY , 52 
)

…or as text:

{MONDAY=52, TUESDAY=52, WEDNESDAY=53, THURSDAY=53, FRIDAY=52, SATURDAY=52, SUNDAY=52}

…how can I invert to produce a multimap of distinct numbers each leading to a collection (list? set?) of the DayOfWeek which owned that number?

The result should be equivalent to the result of this code:

Map.of(
    53 , List.of( DayOfWeek.WEDNESDAY , DayOfWeek.THURSDAY ) ,
    52 , List.of( DayOfWeek.MONDAY , DayOfWeek.TUESDAY , DayOfWeek.FRIDAY , DayOfWeek.SATURDAY , DayOfWeek.SUNDAY ) 
)

I would like to produce the resulting multimap using straight Java without extra libraries such as Eclipse Collections or Google Guava. Those libraries might make this easier, but I am curious to see if a solution using only built-in Java is possible. Otherwise, my Question here is the exact same Question as Guava: construct a Multimap by inverting a Map. Given new streams and multimap features in modern Java, I expect this is possible now while it was not then.

I saw various existing Questions similar to this. But none fit my situation, which seems like a rather common situation. For example, this Question neglects the issue of the original values being redundant/multiple, thus necessitating a multimap as a result. Others such as this or this involve Google Guava.


Solution

  • The following works using Java 9 or above:

    @Test
    void invertMap()
    {
        Map<DayOfWeek, Integer> map = Map.of(
                DayOfWeek.MONDAY, 52,
                DayOfWeek.TUESDAY, 52,
                DayOfWeek.WEDNESDAY, 53,
                DayOfWeek.THURSDAY, 53,
                DayOfWeek.FRIDAY, 52,
                DayOfWeek.SATURDAY, 52,
                DayOfWeek.SUNDAY, 52
        );
    
        Map<Integer, Set<DayOfWeek>> flipped = new TreeMap<>();
        map.forEach((dow, count) ->
                flipped.computeIfAbsent(count, (key) ->
                        EnumSet.noneOf(DayOfWeek.class)).add(dow));
    
        Map<Integer, Set<DayOfWeek>> flippedStream = map.entrySet().stream()
               .collect(Collectors.groupingBy(
                        Map.Entry::getValue, 
                        TreeMap::new,
                        Collectors.mapping(
                                Map.Entry::getKey,
                                Collectors.toCollection(
                                        () -> EnumSet.noneOf(DayOfWeek.class)))));
    
        Map<Integer, Set<DayOfWeek>> expected = Map.of(
                53, EnumSet.of(
                        DayOfWeek.WEDNESDAY, 
                        DayOfWeek.THURSDAY),
                52, EnumSet.of(
                        DayOfWeek.MONDAY, 
                        DayOfWeek.TUESDAY, 
                        DayOfWeek.FRIDAY, 
                        DayOfWeek.SATURDAY, 
                        DayOfWeek.SUNDAY)
        );
        Assert.assertEquals(expected, flipped);
        Assert.assertEquals(expected, flippedStream);
    }
    

    If you are open to using a third-party library, the following code will work with Eclipse Collections:

    @Test
    void invertEclipseCollectionsMap()
    {
        MutableMap<DayOfWeek, Integer> map =
                Maps.mutable.<DayOfWeek, Integer>empty()
                        .withKeyValue(DayOfWeek.MONDAY, 52)
                        .withKeyValue(DayOfWeek.TUESDAY, 52)
                        .withKeyValue(DayOfWeek.WEDNESDAY, 53)
                        .withKeyValue(DayOfWeek.THURSDAY, 53)
                        .withKeyValue(DayOfWeek.FRIDAY, 52)
                        .withKeyValue(DayOfWeek.SATURDAY, 52)
                        .withKeyValue(DayOfWeek.SUNDAY, 52);
    
        SetMultimap<Integer, DayOfWeek> flipped = map.flip();
    
        Assert.assertEquals(flipped.get(52), Set.of(
                DayOfWeek.MONDAY,
                DayOfWeek.TUESDAY,
                DayOfWeek.FRIDAY,
                DayOfWeek.SATURDAY,
                DayOfWeek.SUNDAY));
        Assert.assertEquals(flipped.get(53), Set.of(
                DayOfWeek.WEDNESDAY,
                DayOfWeek.THURSDAY));
    }
    

    Note: I am a committer for Eclipse Collections.