1

Problem:

  • I have a Map<LocalDate, Integer> and I want to group these map elements by week using LocalDate and find the max value of each grouped values by week using Java 8 Stream.

  • I am currently having issues where I can't convert the Key back into LocalDate object in the stream due to the fact that I do not have the required data fields to create object of LocalDate into the Map.

  • If I were to not convert this to LocalDate and keep it as String instead, I cant use the TreeMap to arrange the keys-values in natural order using the keys.

  • If I do not include month into DateTimeFormatter.ofPattern when grouping the data, weeks may overlap between two years.

  • If I only use week number Integer as output of the map from the stream, the grouped values by week will be mixed and overlapped with week numbers from another year, i.e. 2020-2021 with Week 1.

Question:

  • How do I get the max value of values per week using java streams?

DataSet:

Map<LocalDate,Integer> CumulativeGarbageWasteDate = new HashMap<LocalDate,Integer>();
    CumulativeGarbageWasteDate.put(LocalDate.parse(("5/2/2020"),DateTimeFormatter.ofPattern("[M/d/yyyy][M/d/yy]")),2400);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("12/24/20"),DateTimeFormatter.ofPattern("M/d/yy")),140);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("5/2/20"),DateTimeFormatter.ofPattern("M/d/yy")),2400);
    
    CumulativeGarbageWasteDate.put(LocalDate.parse(("01/1/21"),DateTimeFormatter.ofPattern("M/d/yy")),182);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("01/2/21"),DateTimeFormatter.ofPattern("M/d/yy")),203);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("01/3/21"),DateTimeFormatter.ofPattern("M/d/yy")),321);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("01/3/21"),DateTimeFormatter.ofPattern("M/d/yy")),421);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("5/2/2021"),DateTimeFormatter.ofPattern("[M/d/yyyy][M/d/yy]")),2400);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("01/6/21"),DateTimeFormatter.ofPattern("M/d/yy")),1200);
    CumulativeGarbageWasteDate.put(LocalDate.parse(("01/31/21"),DateTimeFormatter.ofPattern("M/d/yy")),2400);

Code:

private static Map<LocalDate, Integer> sumByWeek (Map<LocalDate,Integer> CumulativeGarbageWasteDate){
    
    DateTimeFormatter formatter3 = new DateTimeFormatterBuilder().appendPattern("w/M/YY").parseDefaulting(ChronoField.DAY_OF_WEEK,DayOfWeek.FRIDAY.getValue()).toFormatter();
    

    return CumulativeGarbageWasteDate
            .entrySet()
            .stream()
            .collect(Collectors.groupingBy(
                    row-> LocalDate.parse(row.getKey().format(DateTimeFormatter.ofPattern("w/M/YY")),formatter3),
                    TreeMap::new,
                    Collectors.reducing(0, x->x.getValue(),Math::max)));
}

Error:

Here is the error I encountered when running the code.

java.time.format.DateTimeParseException: Text '5/2/20' could not be parsed: Conflict found: Field MonthOfYear 1 differs from MonthOfYear 2 derived from 2020-01-31
dancisdoted d
  • 13
  • 1
  • 5
  • Welcome to Stack Overflow. I highly recommend you take the [tour](https://stackoverflow.com/tour) to learn how Stack Overflow works and read [How to Ask](https://stackoverflow.com/questions/how-to-ask). This will help you to improve the quality of your questions. For every question, please show the attempts you have tried **and the error messages you get from your attempts.** – McPringle Oct 30 '21 at 10:22
  • @McPringle hi, I have updated the content with error alongside the attempts added earlier and updated the title for better clarification. Thank you for the feedback. – dancisdoted d Oct 30 '21 at 10:33
  • What if two date are same because `Hashmap` does not support duplicate key – Faheem azaz Bhanej Oct 30 '21 at 10:41
  • @FaeemazazBhanej the date will always be unique because the data in the map is meant to represent daily cumulative values from date a to date b. I have no issue initializing the map, the issue lies in trying to use stream to find the max weekly integer and return Map object type. I have updated the question for better clarification – dancisdoted d Oct 30 '21 at 10:42
  • What is your definition of a week? – Bohemian Oct 31 '21 at 03:02
  • @Bohemian in this instance, i would define week as week based off year. i.e. i have date in my map of 29th-30th-31st of October, they would be under a single week and of the 43rd week in 2021. I would want to find the max value of these 3 days that have their respective values in that single week, it could be 30th October with integer of 50 or 31st October with integer of 30. – dancisdoted d Oct 31 '21 at 03:19
  • 2
    Have you considered [this](https://stackoverflow.com/questions/26012434/get-week-number-of-localdate-java-8) method of getting an int value for the week of the year given a LocalDate object ? – duppydodah Oct 31 '21 at 03:25
  • @user3696953 yes i did, but if i only use the integer week. the data grouped together will overlap with week numbers from another year. – dancisdoted d Oct 31 '21 at 03:42
  • Then, you could try using a WeekYear type object which would hold an int for the week & an int for the year ? – duppydodah Oct 31 '21 at 03:46
  • Some of your entries have a 4-digit-year, some have a 2-digit-year. You should either unify that or handle it correctly. – McPringle Oct 31 '21 at 06:56
  • @user3696953 hi thanks for the advice, i tried it and it works perfectly as expected the output, however it is not of localdate object type. Is it impossible to create a local date object and grouping by with just month, week and year? – dancisdoted d Oct 31 '21 at 08:54
  • 1
    @ArvindKumarAvinash did u realize that it hasnt been fully solved yet? why would i post an answer to my question that I haven't got the right answer for? – dancisdoted d Oct 31 '21 at 09:56
  • @dancisdotedd - Oh...from your comment, `...it has been fixed...`, I thought you have solved the problem...carry on. – Arvind Kumar Avinash Oct 31 '21 at 10:23
  • @dancisdotedd Do you want the result to be the map of exact dates with the max count for each week? – Gautham M Oct 31 '21 at 10:46
  • @GauthamM yes that is close to what i want., i would like the result to be `{50/12/19=140,1/1/20=421,2/1/20=1200} ` where `[week of year/month of year/ year= max count for each week]`. Currently following user3696953's advice, i'm able to get `2020-W04=0, 2020-W05=0, 2020-W06=0` in YearWeek object but I want to include month in the output as well in LocalDate object type instead of YearWeek – dancisdoted d Oct 31 '21 at 10:54
  • @dancisdotedd I have added an answer. hope that solves. Note that it returns the date having the highest value in that week for each week. You may convert it `w/M/yy` format if required. – Gautham M Oct 31 '21 at 11:41

1 Answers1

0

The output map will be of the type Map<LocalDate, Integer>, where the key would be the date which had the highest value for that week. (I am splitting to solution to make it more understandable).

First I would suggest the creation of a class (may be an inner class) to hold the date and it's corresponding value. This would make the processing easy.

private static class MyData {
    private LocalDate date;
    private int value;
    
    // getters and a constructor
}

Note: Assume static import for Collectors methods like grouping, toMap, toList, mapping in the below codes.


Week of the year could be retrieved using WeekFields.(Refer)

NOTE: row.getKey().getYear() should not be used in the below code instead of row.getKey().get(weekFields.weekBasedYear()) to fetch the year as it could yield incorrect results in the scenarios like: 2022-01-01 and 2021-12-31, belongs to week 1 of 2022. So if we use getYear(), then the respective generated string would be 1/2022 and 1/2021, which is incorrect. The expected string in this case would be 1/2022.

WeekFields weekFields = WeekFields.of(Locale.getDefault());

// function to generate a key of the form: week~year. year included to avoid overlap
Function<Entry<LocalDate, Integer>, String> groupFn 
    = row -> String.format("%d/%d",
                           row.getKey().get(weekFields.weekOfWeekBasedYear()),
                           row.getKey().get(weekFields.weekBasedYear()));

Now convert the input into MyData objects:

Map<String, List<MyData>> temp = 
    input.entrySet()
         .stream()
         .collect(groupingBy(groupFn,
                             TreeMap::new, 
                             mapping(e -> new MyData(e.getKey(), e.getValue()), toList())));

Now we find the date with max value within a week:

Function<Entry<String, List<MyData>>, MyData> maxDataFn 
    = entry -> entry.getValue()
                    .stream()
                    .collect(collectingAndThen(maxBy(Comparator.comparingInt(MyData::getValue)),
                                               Optional::get));

TreeMap<LocalDate, Integer> result 
    = temp.entrySet()
          .stream()
          .map(maxDataFn)
          .collect(toMap(MyData::getDate, MyData::getValue, Integer::max, TreeMap::new));

Gautham M
  • 4,816
  • 3
  • 15
  • 37
  • Could you share how would you do the conversion to `w/M/yy` in this? – dancisdoted d Oct 31 '21 at 15:58
  • @dancisdotedd Can you mention the use case of displaying it as w/M/yy. Is to return as part of an API ? The LocalDate object contains all necessary information about the week. So you could just convert it to the required format using the usual formatter itself. – Gautham M Oct 31 '21 at 17:08
  • 1
    ah, i was planning to have its localdate output to be only specifically w/M/yy, but that would not be possible since there is no days present to parse as a localdate. But formatting the string output as w/M/yy works. – dancisdoted d Nov 01 '21 at 05:51
  • @dancisdotedd Yes you could keep it as the usual `LocalDate` and format it to String when required. I assume that the formatting to week is to be used only for display purposes. For other processing it is better to use the LocalDate, as you could always derive the week information from that. – Gautham M Nov 01 '21 at 06:01
  • 1
    Instead of 'stringing' it together, you could also create a class (or record) which represents a `YearWeek`. – MC Emperor Nov 01 '21 at 08:09
  • 1
    Important: instead of `row.getKey().getYear()`, you should use `row.getKey().get(weekFields.weekBasedYear())`, otherwise the year/week-of-year-combination is strange when the week number of a date on a certain year belongs to another year. For example, 2022-01-01, belongs to week 52 of 2021, with your current code this would yield `2022-W52`, while it should be `2021-W52`. – MC Emperor Nov 01 '21 at 11:32
  • @GauthamM thanks for the updated answers and clarifications, i would like to ask about `Integer::max` as i'm rather confused by this. I understand that maxDataFn will return the highest MyData object from the list of each entry instance, but why is `Integer::max` used in this case? – dancisdoted d Nov 03 '21 at 16:26
  • @dancisdotedd It is a mergeFunction which is used to pick a value when there are multiple values associated with the same key. This is actually not relevant in your use case, as each `MyDate` would have a unique `date` element. – Gautham M Nov 03 '21 at 16:36
  • I see, i assume this was needed in order to allow the use of `TreeMap::new` which otherwise wouldnt be allowed without the use of the binaryoperator | mergeFunction of `Integer::Max` – dancisdoted d Nov 04 '21 at 04:41
  • Yes, there is only one version of `toMap` method which allows a Supplier to create a Map with a type of our choice. And that requires the merging function as well – Gautham M Nov 04 '21 at 05:51