Search code examples
javasortingguavacomparator

ImmutableSortedMap - Duplicate keys in mappings


I have an ImmutableSortedMap that holds data in the following datastructure.

<String, Pair<Long, Double>>

My goal is to sort this map in descending order starting with the largest Double and if two Doubles are the same, sort it by the Long. If both the Double and Long are the same, sort it by String

My current comparator only deals with the first case, and looks like this.

private ImmutableSortedMap<String, Pair<Long, Double>> sortInDescendingSizeOrder(final Map<String, Pair<Long, Double>> statsByType) {
    Ordering<String> ordering = Ordering
            .from(new Comparator<Pair<Long, Double>>() {
                @Override
                public int compare(Pair<Long, Double> o, Pair<Long, Double> o2) {
                    return o.getRight().equals(o2.getRight()) ? o.getLeft().compareTo(o2.getLeft()) : o.getRight().compareTo(o2.getRight());
                }
            })
            .reverse()
            .onResultOf(Functions.forMap(statsByType, null));

    return ImmutableSortedMap.copyOf(statsByType, ordering);
}

However, it finds two Doubles that are the same and throws the following exception: Exception in thread "main" java.lang.IllegalArgumentException: Duplicate keys in mappings

I don't understand what am I doing wrong...

EDIT: I have tried to add the compound method and order by String (at least).

.compound(new Comparator<String>() {
                    @Override
                    public int compare(String o1, String o2) {
                        return o1.compareTo(o2);
                    }
                });

This makes the code not throw any Exception, however, there is no ordering whatsoever in the resulted map.

EDIT 2: At this point, any ordering like the one described above of a

Map<String, Pair<Long, Double>> 

will do. Doesn't necessarily need to be an ImmutableSortedMap


Solution

  • EDIT: See Louis' answer for actual proper solution (ImmutableMap.Builder#orderEntriesByValue(Comparator)). Continue reading this answer for fix of an incorrect approach ;)


    It's a documented behavior of ImmutableSortedMap#copyOf(Map, Comparator):

    Throws:

    NullPointerException - if any key or value in map is null
    IllegalArgumentException - if any two keys are equal according to the comparator

    What you get is a result of values being equal according to comparator, so the problem is there. See this answer for comparing map by value - as Louis mentions (he's from Guava team), using Functions.forMap is tricky.

    In your case adding .nullsLast() to first ordering and appending .compound(Ordering.natural()) should work. For example:

    Map<String, Pair<Long, Double>> map = ImmutableMap.of(
            "a", Pair.of(1L, 1.0d),
            "b", Pair.of(1L, 1.0d),
            "c", Pair.of(1L, 1.0d),
            "d", Pair.of(1L, 1.0d)
    );
    
    @Test
    public void should2()
    {
        final ImmutableSortedMap<String, Pair<Long, Double>> sortedMap = sortInDescendingSizeOrder(map);
        assertThat(sortedMap).hasSize(4);
        System.out.println(sortedMap); // {a=(1,1.0), b=(1,1.0), c=(1,1.0), d=(1,1.0)}
    }
    

    and for

    Map<String, Pair<Long, Double>> map = ImmutableMap.of(
            "a", Pair.of(1L, 1.0d),
            "b", Pair.of(2L, 1.0d),
            "c", Pair.of(1L, 2.0d),
            "d", Pair.of(2L, 2.0d)
    );
    

    it outputs {d=(2,2.0), b=(2,1.0), c=(1,2.0), a=(1,1.0)}.

    (sortInDescendingSizeOrder for reference)

    private ImmutableSortedMap<String, Pair<Long, Double>> sortInDescendingSizeOrder(
            final Map<String, Pair<Long, Double>> statsByType) {
        Ordering<String> ordering = Ordering.from(new Comparator<Pair<Long, Double>>() {
                    @Override
                    public int compare(Pair<Long, Double> o, Pair<Long, Double> o2) {
                        return ComparisonChain.start()
                                .compare(o.getRight(), o2.getRight())
                                .compare(o.getLeft(), o2.getLeft())
                                .result();
                    }
                })
                .reverse()
                .nullsLast()
                .onResultOf(Functions.forMap(statsByType, null))
                .compound(Ordering.natural());
    
        return ImmutableSortedMap.copyOf(statsByType, ordering);
    }
    

    ComparisonChain is another goodie from Guava you can use here.