4

I have been working with Maps at present and I am baffled by how I can get my program to work effectively. I can iterate over the map get the keys and values and sort them in alphabetical and reverse alphbetical order quite easily and have used custom comparators for this. However, I am now trying to sort the map based on the key with the most values. The values are a list of objects I have created and can be thought of as this scenario.

There is an Atlas(like a catalog) that has lots of towns (the key of type string). That contains Shops(List). I want to sort this so that the town with the most shops is displayed first and goes in descending order with the secondary sorting being based on town alphabetically and return a string representing this.

I have used the Comparator interface with seperate classes for each one alphabetically and reverse alphabetically so far and wish to follow the same pattern for learning purposes However this has me completely stumped.

Example:

class Atlas {

       Map<String, List<Shop> atlas = new HashMap<String, List<Shop>();

       void addShop(Shop shop){
            //if(Atlas already contains){
              get the town and add the shop to it.
            }
            else{
                 add the town as the key and the shop as the value in the list
            }
       }

       List<Shop> getAllShopsFromTheGivenTown(String givenTown){
            //if(Atlas contains givenTown){
            return the givenTown from the List. 
            }
            else{
                 //Return an ArrayList emptyList
            }
       }

       public String returnAllTownsAndShopsAlphbetically(){
       String tmpString = "";   

    List<String> keys = new LinkedList<String>(atlas.keySet());
    TownComparatorAtoZ tc = new TownComparatorAtoZ();
    Collections.sort(keys, tc);

    for(String town : keys){
         List<Shop> shops = new LinkedList<Dealer>(atlas.get(town));
         ShopComparatorAtoZ sc = new ShopComparatorAtoZ();
          Collections.sort(shop, sc);

        for(Shop shop : shops){
            if(tmpString.isEmpty()){
            tmpString = tmpString + town + ": " + shop.getName();
            }
            else if(tmpString.contains(town)){
            tmpString = tmpString + ", " + shop.getName();
            }
            else{
            tmpString = tmpString + " | " + town + ": " + shop.getName();               }   
        }
    }       
    return tmpString;   
    }
}

As can be seen from above (although not the cleanest and most efficient) returns things alphabetically and will be reformatted into a string builder. However, I am wondering how I can use a comparator to achieve what I am after and if someone could provide a code snippet with an explanation of what it actually does I would be grateful as its more about understanding how to do it not just getting a copy and pasted lump of code but need to see if visually in code to understand it.

SO output I want to be something like

manchester: m&s, h&m, schuch | birmingham: game, body shop | liverpool: sports

Bhavik Ambani
  • 6,557
  • 14
  • 55
  • 86
BigRikk
  • 43
  • 1
  • 5
  • After a certain collections complexity, Lists or Maps of a class make more sense. The class would contain the collection relationships. – Gilbert Le Blanc Dec 21 '12 at 16:14
  • At the moment my thinking has been to give an instance to the comparator but just wondering how I could manage that at present. IN terms of "class would contain the collections relationship" are you meaning extending from the class for the comparator? Just baffled at the moment and it is Friday lol? – BigRikk Dec 21 '12 at 16:26
  • Right now, you have a Map defined. Make a class, TownShops, that represents List. Your Map collection would be Map. You already know how to write a Comparator to sort that. – Gilbert Le Blanc Dec 21 '12 at 18:09

2 Answers2

2

You can try something like this:

public static Map<String, List<Shop>> mySortedMap(final Map<String, List<Shop>> orig)
{
    final Comparator<String> c = new Comparator<String>()
    {
        @Override
        public int compare(final String o1, final String o2)
        {
            // Compare the size of the lists. If they are the same, compare
            // the keys themsevles.
            final int sizeCompare = orig.get(o1).size() - orig.get(o2).size();
            return sizeCompare != 0 ? sizeCompare : o1.compareTo(o2);
        }
    }

    final Map<String, List<Shop>> ret = new TreeMap<String, List<Shop>>(c);
    ret.putAll(orig);
    return ret;
}

Explanation: TreeMap is the basic implementation of a SortedMap, and it can take a comparator of key values as an argument (if no comparator is passed as an argument, natural ordering of the keys prevails). Here we create an ad hoc comparator comparing the list sizes of the original map passed as an argument, and if the sizes are equal, it compares the keys themselves. Finally, we inject all elements from the origin map into it, and return it.

fge
  • 119,121
  • 33
  • 254
  • 329
  • Note however that the whole map will likely break badly if any of the value lists change in length while they are members of the map, since the ordering will no longer match the tree structure. – Ian Roberts Dec 21 '12 at 16:56
  • @IanRoberts: true enough, but hey, if this is a requirement that lists don't change, the programmer has to do it by itself ;) I happen to love Guava's `ImmutableList` for that. – fge Dec 21 '12 at 16:59
  • Will give it a go now and post in the morning too, thank you very much :), I don't have the luxury of guava unfortunately as learning this for work from scratch. – BigRikk Dec 21 '12 at 17:00
  • @BigRikk: post edited so that keys are compared if list sizes are equal. – fge Dec 21 '12 at 17:01
  • @SteveKuo matter of taste! – fge Dec 21 '12 at 17:23
  • Just looking and with this its comparing the keys and returns the order in alphabetical but I want to order the list by the keys with the most values and not by the keys alphbetically in first instance. I want to order by town with the most shops first and if these are equal then sort by alphabetical order. – BigRikk Dec 24 '12 at 10:48
  • @BigRikk well, this is exactly what the comparator does: it first compares the list sizes, and compares the town names if and only if the list sizes are equal. – fge Dec 24 '12 at 10:56
  • Thanks you I have managed to implement this effectively and refactor everything accordingly. Note: Overall Code has gone from 250 lines to 110 by re-factoring so very very useful. – BigRikk Jan 02 '13 at 10:22
0

What if you try something like the following:

private static final Comparator<Map.Entry<String, List<Shop>>> CountThenAtoZ =
    new Comparator<Map.Entry<String, List<Shop>>>() {
        @Override
        public int compare(Map.Entry<String, List<Shop>> x, Map.Entry<String, List<Shop>> y) {
            // Compare shop count first. If equal, compare keys alphabetically.
            int cmp = ((Integer)x.getValue().size()).compareTo(y.getValue().size());
            return cmp != 0 ? cmp : x.getKey().compareTo(y.getKey());
        }
    };

...

public String returnAllTownsAndShopsAlphbetically() {

    List<Map.Entry<String, List<Shop>>> entries = new ArrayList<>(atlas.entrySet());
    Collections.sort(entries, CountThenAtoZ);

    String result = "";
    boolean firstTown = true;
    for (Map.Entry<String, List<Shop>> entry : entries) {
        if (!firstTown) result += " | "; else firstTown = false;
        result += entry.getKey() + ": ";

        boolean firstShop = true;
        TreeSet<Shop> sortedShops = new TreeSet<>(new ShopComparatorAtoZ());
        sortedShops.addAll(entry.getValue());
        for (Shop shop : sortedShops) {
            if (!firstShop) result += ", "; else firstShop = false;
            result += shop.getName();
        }
    }

    return result;
}

The way this works is to first create a list of the atlas entries in exactly the order we want. We need access to both the keys and their associated values to build the correct ordering, so sorting a List of Map.Entry instances is the most convenient.

We then walk the sorted list to build the resulting String, making sure to sort the shops alphabetically before adding them to the String.

cambecc
  • 4,083
  • 1
  • 23
  • 24
  • Integer will not take a compare for entries on it within eclipse. Sorting by alphabetical order is not the issue. I want to sort by most shops in a town then if a town has equal shops then alphabetical. – BigRikk Dec 24 '12 at 10:49
  • Ahh right. `Integer.compare(int, int)` was added in Java 7. I've adjusted the code above so it works on Java 6. – cambecc Dec 24 '12 at 13:01
  • Yes, if you inspect the `CountThenAtoZ` comparator, you will see that it sorts by most shops in a town, then alphabetically when shop count is equal. That is how towns are ordered. We alphabetically sort shops _within a town_ only when building the String, just as your original example does. – cambecc Dec 24 '12 at 13:06