Search code examples
javajava-stream

How to collect a map of maps from stream


I have below class

public class Data {
    
    String name;
    String dateOfBirth;
    String dateAttendedClass;

}

I have a list of Data and wanted to create a map of below:

Map<String Map<String, List<String>>>

Map<dateOfBirth Map<name,list(dateAttendedClass)>>

I managed to create a Map of

Map<name,list(dateAttendedClass)>

by doing this

    Map<String, List<String>> nameAttendedClassMap = data.stream()
            .collect(Collectors.groupingBy(Data::getName,
                    Collectors.mapping(Data::getDateAttended, Collectors.toList())));

But I am not sure how to collect this map further and have it in a map with the key being date of birth.

Map<String Map<String, List<String>>>

Solution

  • As suggested by others, you should nest your groupingBy calls.

    Here is a working example. You should store your dates as epoch values. This is the best way to store the values. They are more transportable and universal this way. If you want to display any information, you can call convenience methods to format. When a date is represented as an integer, you can specify what locale you want to use to format the date. There is also no need to pare the dates when comparing them when sorting; or comparing in general.

    Also, name your objects appropriately. Changing the name of Data to Student helps identify what the object represents.

    package org.example;
    
    import java.time.*;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.stream.Collectors;
    
    import com.google.gson.GsonBuilder; // Used for printing formatted JSON
    
    public class DataMap {
        private static final DateTimeFormatter dateFormatter;
        private static final DateTimeFormatter yearFormatter;
    
        static {
            dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            yearFormatter = DateTimeFormatter.ofPattern("yyyy");
        }
    
        public static void main(String[] args) {
            List<Student> data = new ArrayList<Student>();
            data.add(createStudent("Billy", "1990-01-30", "2005-08-28"));
            data.add(createStudent("Billy", "1990-01-30", "2006-08-28"));
            data.add(createStudent("Billy", "1990-01-31", "2007-08-28"));
            data.add(createStudent("Billy", "1990-01-31", "2008-08-28"));
            data.add(createStudent("Alice", "1990-01-30", "2005-08-28"));
            data.add(createStudent("Alice", "1990-01-30", "2006-08-28"));
            data.add(createStudent("Alice", "1990-01-31", "2007-08-28"));
            data.add(createStudent("Alice", "1990-01-31", "2008-08-28"));
    
            Map<String, Map<String, List<String>>> grouped = groupAttendanceByBirthAndName(data);
            System.out.println(grouped);
    
            // For display purposes...
            System.out.println(new GsonBuilder().setPrettyPrinting().create().toJson(grouped).toString());
        }
    
        private static Map<String, Map<String, List<String>>> groupAttendanceByBirthAndName(List<Student> students) {
            return students.stream()
                    .collect(Collectors.groupingBy(Student::getDateOfBirthAsString,
                            Collectors.groupingBy(Student::getName,
                                    Collectors.mapping(Student::getDateAttendedClassAsString, Collectors.toList()))));
        }
    
        private static String formatEpochMilliAsDate(long epochMilli, DateTimeFormatter dateFormatter) {
            return dateFormatter.format(Instant.ofEpochMilli(epochMilli).atZone(ZoneId.systemDefault()).toLocalDate());
        }
    
        private static long epochFromDate(String date) {
            return LocalDate.parse(date).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
        }
    
        private static Student createStudent(String name, String birthYear, String attendanceYear) {
            return new Student(name, epochFromDate(birthYear), epochFromDate(attendanceYear));
        }
    
        private static class Student {
            private String name;
            private long dateOfBirth;
            private long dateAttendedClass;
    
            public Student(String name, long dateOfBirth, long dateAttendedClass) {
                this.name = name;
                this.dateOfBirth = dateOfBirth;
                this.dateAttendedClass = dateAttendedClass;
            }
    
            public String getName() {
                return name;
            }
    
            public long getDateOfBirth() {
                return dateOfBirth;
            }
    
            // Convenience method ~ Used for grouping and display
            public String getDateOfBirthAsString() {
                return formatEpochMilliAsDate(dateOfBirth, dateFormatter);
            }
    
            public long getDateAttendedClass() {
                return dateAttendedClass;
            }
    
            // Convenience method ~ Used for grouping and display
            public String getDateAttendedClassAsString() {
                return formatEpochMilliAsDate(dateAttendedClass, yearFormatter);
            }
        }
    }
    

    Output

    Raw Map output:

    {1990-01-31={Billy=[2007, 2008], Alice=[2007, 2008]}, 1990-01-30={Billy=[2005, 2006], Alice=[2005, 2006]}}
    

    I used Google's Gson library to pretty-print the nested map as JSON. You could use Jackson's ObjectMapper as well.

    {
      "1990-01-31": {
        "Billy": [
          "2007",
          "2008"
        ],
        "Alice": [
          "2007",
          "2008"
        ]
      },
      "1990-01-30": {
        "Billy": [
          "2005",
          "2006"
        ],
        "Alice": [
          "2005",
          "2006"
        ]
      }
    }