Sort a map with "MMMyyyy" as key
Asked Answered
C

3

2

I have a map whose keys are in MMMyyyy format and i need to sort based on month. input:

unsorted: {
 "Dec2010": 1,
 "Apr2010": 1,
 "Feb2010": 0,
 "Nov2010": 2,
 "Mar2010": 0,
 "Jun2010": 2,
 "Sep2010": 1,
 "May2010": 0,
 "Oct2010": 1,
 "Jul2010": 0,
 "Aug2010": 0,
 "Jan2010": 1
}

After sorting it should be like below:

sorted: {
 "Jan2010": 1
 "Feb2010": 0,
 "Mar2010": 0,
 "Apr2010": 1,
 "May2010": 0,
 "Jun2010": 2,
 "Jul2010": 0,
 "Aug2010": 0,
 "Sep2010": 1,
 "Oct2010": 1,
 "Nov2010": 2,
 "Dec2010": 1,     
}

Currently i am using treemap which sorts it in alphabetical order but how do i get it sorted based on month hierarchy.

Code:

    Map<String, Integer> unsorted = new HashMap<>();
    unsorted.put("Dec2010", 1);
    unsorted.put("Apr2010", 1);
    unsorted.put("Feb2010", 0);
    unsorted.put("Nov2010", 2);
    unsorted.put("Mar2010", 0);
    unsorted.put("Jun2010", 2);
    unsorted.put("Sep2010", 1);
    unsorted.put("May2010", 0);
    unsorted.put("Oct2010", 1);
    unsorted.put("Jul2010", 0);
    unsorted.put("Aug2010", 0);
    unsorted.put("Jan2010", 1);

    System.out.println("\nSorted......");
    Map<String, Integer> sorted = new TreeMap<>(unsorted);
    for (Map.Entry<String, Integer> entry : sorted.entrySet()) {
        System.out.println("Key : " + entry.getKey()
                + " Value : " + entry.getValue());
    }
Croix answered 13/8, 2015 at 20:50 Comment(3)
Can you share whatever you have done so far?Goal
I have updated the current sample code i am using.Croix
Don’t store your months and years as strings in your map. Use proper date objects. In this case this would mean YearMonth objects. Only when you need to give string output, format the YearMonth into a string in the required format.Uracil
D
5

Let's try with a custom comparator:

public class Main {

    public static void main(String[] args) {

        final SimpleDateFormat df = new SimpleDateFormat("MMMyyyy");

        Map<String, Integer> map = new TreeMap<>(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                String s1 = (String) o1;
                String s2 = (String) o2;
                try {
                    return df.parse(s1).compareTo(df.parse(s2));
                } catch (ParseException e) {
                    throw new RuntimeException("Bad date format");
                }
            };
        });

        map.put("Dec2011",1);
        map.put("Jan2011",0);
        map.put("Feb2011",1);
        map.put("Mar2011",0);
        map.put("Oct2011",1);
        map.put("Sep2011",0);

        for(Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

    }
}

Output:

Jan2011 -> 0
Feb2011 -> 1
Mar2011 -> 0
Sep2011 -> 0
Oct2011 -> 1
Dec2011 -> 1
Draconic answered 13/8, 2015 at 21:10 Comment(2)
Thanks mzc, this is what i was looking for.Croix
I think you can simplified to Map<String, Integer> map = new TreeMap<>((o1, o2) -> { try { return df.parse(o1).compareTo(df.parse(o2)); } catch (ParseException e) { throw new RuntimeException("Bad date format"); } });Mump
F
3

The legacy date-time API (java.util date-time types and their formatting API, SimpleDateFormat) is outdated and error-prone. It is recommended to stop using it completely and switch to java.time, the modern date-time API*.

Java SE 8

import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> unsorted = new HashMap<>();
        unsorted.put("Dec2010", 1);
        unsorted.put("Apr2010", 1);
        unsorted.put("Feb2010", 0);
        unsorted.put("Nov2010", 2);
        unsorted.put("Mar2010", 0);
        unsorted.put("Jun2010", 2);
        unsorted.put("Sep2010", 1);
        unsorted.put("May2010", 0);
        unsorted.put("Oct2010", 1);
        unsorted.put("Jul2010", 0);
        unsorted.put("Aug2010", 0);
        unsorted.put("Jan2010", 1);

        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("MMMuuuu", Locale.ENGLISH);
        Comparator<String> comparator = (s1, s2) -> YearMonth.parse(s1, dtf).compareTo(YearMonth.parse(s2, dtf));
        Map<String, Integer> sorted = new TreeMap<>(comparator);
        sorted.putAll(unsorted);
        System.out.println(sorted);
    }
}

Output:

{Jan2010=1, Feb2010=0, Mar2010=0, Apr2010=1, May2010=0, Jun2010=2, Jul2010=0, Aug2010=0, Sep2010=1, Oct2010=1, Nov2010=2, Dec2010=1}

Learn more about the the modern date-time API* from Trail: Date Time.

Java SE 9

Java SE 9 introduced Map.ofEntries which you can use to initialize your Map<String, Integer> unsorted.

import static java.util.Map.entry;

import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> unsorted = Map.ofEntries(
                                            entry("Dec2010", 1),
                                            entry("Apr2010", 1),
                                            entry("Feb2010", 0),
                                            entry("Nov2010", 2),
                                            entry("Mar2010", 0),
                                            entry("Jun2010", 2),
                                            entry("Sep2010", 1),
                                            entry("May2010", 0),
                                            entry("Oct2010", 1),
                                            entry("Jul2010", 0),
                                            entry("Aug2010", 0),
                                            entry("Jan2010", 1)
                                        );

        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("MMMuuuu", Locale.ENGLISH);
        Comparator<String> comparator = (s1, s2) -> YearMonth.parse(s1, dtf).compareTo(YearMonth.parse(s2, dtf));         
        Map<String, Integer> sorted = new TreeMap<>(comparator); 
        sorted.putAll(unsorted);
        System.out.println(sorted);
    }
}

Sorting & mapping using a single statement:

You can make use of the powerful Stream API (tutorials: 1, 2) to get your job done.

import static java.util.Map.entry;

import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> unsorted = Map.ofEntries(
                                            entry("Dec2010", 1),
                                            entry("Apr2010", 1),
                                            entry("Feb2010", 0),
                                            entry("Nov2010", 2),
                                            entry("Mar2010", 0),
                                            entry("Jun2010", 2),
                                            entry("Sep2010", 1),
                                            entry("May2010", 0),
                                            entry("Oct2010", 1),
                                            entry("Jul2010", 0),
                                            entry("Aug2010", 0),
                                            entry("Jan2010", 1)
                                        );

        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("MMMuuuu", Locale.ENGLISH);
        Map<String, Integer> sorted = unsorted
                                        .entrySet()
                                        .stream()
                                        .collect(Collectors.toMap(
                                            e -> YearMonth.parse(e.getKey(), dtf),
                                            e -> e.getValue(),
                                            (v1, v2) -> v1,
                                            TreeMap::new
                                         )) // Returns TreeMap<YearMonth, Integer>
                                        .entrySet()
                                        .stream()
                                        .collect(Collectors.toMap(
                                            e -> dtf.format(e.getKey()),
                                            e -> e.getValue(),
                                            (v1, v2) -> v1,
                                            LinkedHashMap::new
                                         ));
        
        System.out.println(sorted);
    }
}

Learn more about Collectors#toMap from the documentation.


* For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7. If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Feingold answered 3/5, 2021 at 16:26 Comment(0)
R
2

Actually, the answer is straightforward and quite simple using TreeMap and a custom Comparator

Map<String, Integer> map = new HashMap<>();
map.put("Dec2010", 1);
map.put("Apr2010", 1);
map.put("Feb2010", 0);
map.put("Nov2010", 2);
map.put("Mar2010", 0);
map.put("Jun2010", 2);
map.put("Sep2010", 1);
map.put("May2010", 0);
map.put("Oct2010", 1);
map.put("Jul2010", 0);
map.put("Aug2010", 0);
map.put("Jan2010", 1);

Map<String, Integer> sortedMap = new TreeMap<>(Comparator.comparing(text -> YearMonth.parse(text, DateTimeFormatter.ofPattern("MMMyyyy", Locale.ENGLISH))));
sortedMap.putAll(map);

sortedMap.entrySet().forEach(System.out::println);

As suggested by Arvind Kumar Avinash, here is an example using Eclipse Collections

The example is quite similar except it has an overloaded constructur accepting the source map as second argument

Map<String, Integer> sortedMap = new TreeSortedMap<>(
    Comparator.comparing(text -> YearMonth.parse(text, DateTimeFormatter.ofPattern("MMMyyyy", Locale.ENGLISH))), 
    map
);

sortedMap.entrySet().forEach(System.out::println);

Both result in

Jan2010=1
Feb2010=0
Mar2010=0
Apr2010=1
May2010=0
Jun2010=2
Jul2010=0
Aug2010=0
Sep2010=1
Oct2010=1
Nov2010=2
Dec2010=1
Reedbuck answered 3/5, 2021 at 17:18 Comment(2)
Nice single-statement solution using Eclipse Collections! Is there a shorter way without using Eclipse Collections of what I've done using single-statement? I might have missed something.Feingold
Here is another nice solution by Yassin where he has used Eclipse Collections.Feingold

© 2022 - 2024 — McMap. All rights reserved.