Search code examples
javafunctional-programmingjava-streamlinkedhashmap

Put the value into the inner map of a map


I have a map of maps: Map<String,<LocalDate,List<Integer>>. In each iteration of loops, I need to insert the daily cases of each country from the List<List<String> into each day.Each day will only limit to one record. But my current attempt is fail because it will append all the list in the first date in the table.

List<List<String>

[Province/State, Country/Region, Lat, Long, 1/22/20, 1/23/20, 1/24/20, 1/25/20, 1/26/20, 1/27/20]
[, Afghanistan, 33.93911, 67.709953, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]
[, Angola, -11.2027, 17.8739, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0]
[, Angeria, -12.3047, 17.8739, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0]
[, Andora, -13.2087, 17.8739, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0]

Desired Output

Afghanistan --> {2020-01-22=[0], 2020-01-23=[0], 2020-01-24=[3], 2020-01-25=[0], 2020-01-26=[0]}
Angola --> {2020-01-22=[0], 2020-01-23=[0], 2020-01-24=[0], 2020-01-25=[0], 2020-01-26=[0]} 
Angeria --> {2020-01-22=[0], 2020-01-23=[0], 2020-01-24=[0], 2020-01-25=[0], 2020-01-26=[0]} 
Andora --> {2020-01-22=[0], 2020-01-23=[0], 2020-01-24=[0], 2020-01-25=[0], 2020-01-26=[0]} 

My attempt

 Map<String, Map<LocalDate, List<Integer>>> dataMap = new LinkedHashMap<>();
 Map<LocalDate,List<Integer>> innerMap = new LinkedHashMap<>();
        IntStream //functional for loop to add the date as keys into the map
                .range(0,covidListWithoutCountryDetails.get(0).size())
                .forEach(i->
                        innerMap
                                .put(keys.get(i),new ArrayList<>()
                                )
                );
IntStream  //functional for loop to add the country keys into map 
    .range(0,mapKeys.size())
    .forEach(i->dataMap
    .put(mapKeys.get(i), innerMap));

Solution

  • First of all, I would suggest using an implementation of .zip() which would allow you to combine two streams into one. It vastly simplifies creating maps, since you can combine a stream of keys and values.

    I will assume a zip implementation with signature similar to the one found in Guava is available:

    public static <A,B,R> Stream<R> zip(Stream<A> streamA,
                                        Stream<B> streamB,
                                        BiFunction<? super A,? super B,R> function)
    

    Using that there are two very simple overloaded helpers that can be derived to create maps from streams of keys and values or a single stream of entries:

    private static <K, V> Map<K, V> toMap(Stream<K> keys, Stream<V> values) {
        return toMap(zip(keys, values, AbstractMap.SimpleEntry::new));
    }
    
    private static <K, V> Map<K, V> toMap(Stream<Map.Entry<K, V>> entries) {
        return entries
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }
    

    With this in place, the rest of the code is few chains of transformations that of the data that is then combined in the maps. This is split into three methods for readability, it can refactored to be split further if needed or the methods can be inlined:

    /**
     * Convert the table data into a breakdown per country
     * @param input - data in table format. First entry contains the headings, 
     * the rest are the rows with corresponding data
     * @return breakdown where each country has number of cases per day as list
     */
    public static Map<String, Map<String, List<Integer>>> transform(List<List<String>> input) {
        List<String> labels = transformDateStrings(input.get(0));
    
        Stream<Map.Entry<String, Map<String, List<Integer>>>> outer = input.stream()
            .skip(1) //skip the header data
            .map(data -> {
                Map<String, List<Integer>> innerMap = transformToInnerMap(labels, data);
                String country = data.get(1);
    
                return new AbstractMap.SimpleEntry(country, innerMap);
            });
    
        return toMap(outer);
    }
    
    /**
     * Transform the dates in the headings to proper ISO 8601 format showing a date.
     * @param headings  - list of strings in <month>/<day>/<year> format
     * @return list of strings in <year>-<month>-<day> 
     */
    private static List<String> transformDateStrings(List<String> headings) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M/d/yy");
    
        return headings.stream()
            .skip(4) // ignore the non-date fields
            .map(str -> LocalDate.parse(str, formatter))
            .map(date -> date.format(DateTimeFormatter.ISO_LOCAL_DATE))
            .collect(Collectors.toList());
    }
    
    /** 
     * Transform input like
     * [, Afghanistan, 33.93911, 67.709953, 0, 0, 3, 0, 0, 0, ]
     * into only labelled values
     * @param labels - string dates to use for labels of each value
     * @param data   - row of information to transform
     * @return a map in format { 2020-01-22=[0], 2020-01-23=[0] }
     */
    private static Map<String, List<Integer>> transformToInnerMap(List<String> labels, 
                                                                  List<String> data) {
        Stream<List<Integer>> values = data.stream()
            .skip(4) //skip fields that do not belong to dates
            .map(Integer::valueOf)
            .map(Collections::singletonList); //convert to immutable list
        
        return toMap(labels.stream(), values);
    }