Bernie
Bernie

Reputation: 1651

Obtain the Count per Month from the Count per Day using Java Streams and groupingBy()

I want to transform a list with per-day sales (whatever, doesn't matter) into a list of per-month sales.

So I have a List<Map<String, Object>> perDay, which contains these daily values:

[{DATE=2022-01-01, COUNT=5}, {DATE=2022-01-02, COUNT=3}, {DATE=2022-02-29, COUNT=4}]

These values should be gathered into monthly values, like that:

[{DATE=2022-01, COUNT=8}, {DATE=2022-02, COUNT=4}]

I want to use groupingBy() method with streams.

Here's my code:

    Map<String, Object> jan1 = new HashMap<>();
    jan1.put("DATE", "2022-01-01"); jan1.put("COUNT", "5");
    Map<String, Object> jan2 = new HashMap<>();
    jan2.put("DATE", "2022-01-02"); jan2.put("COUNT", "3");
    Map<String, Object> feb1 = new HashMap<>();
    feb1.put("DATE", "2022-02-29"); feb1.put("COUNT", "4");
    List<Map<String, Object>> perDay = List.of(jan1, jan2, feb1);

Map<String, Object> perMonth = perDay.stream()
        .collect(Collectors.groupingBy("???", 
                    Collectors.summingInt(f -> Integer.parseInt((String) f))));

I guess, I'm close, but I'm not quite there yet.

Any ideas?

Upvotes: 0

Views: 697

Answers (3)

WJS
WJS

Reputation: 40062

First, I would recommend against using a list of maps either for date sales, or for month sales.

Here is your data.

Map<String, Object> jan1 = new HashMap<>();
jan1.put("DATE", "2022-01-01");
jan1.put("COUNT", "5");
Map<String, Object> jan2 = new HashMap<>();
jan2.put("DATE", "2022-01-02");
jan2.put("COUNT", "3");
Map<String, Object> feb1 = new HashMap<>();
feb1.put("DATE", "2022-02-29");
feb1.put("COUNT", "4");
List<Map<String, Object>> perDay = List.of(jan1, jan2, feb1);

And here is how you would do it.

  • stream the maps
  • take the date and use only the year/month part
  • convert the count to an integer (so it can be summed)
  • and when duplicate keys are encountered, add the counts
  • note that this involves casting of values.
Map<String, Integer> map = perDay.stream()
        .collect(Collectors.toMap(
                mp -> ((String) mp.get("DATE")).substring(0,
                        7),
                mp -> Integer
                        .valueOf((String) mp.get("COUNT")),
                Integer::sum));

But your initial List<Map<String,Object>> doesn't let you choose the date since there is no relationship to the DATE value of the map to the index of the list. However, if you stored them as a Map<String,Integer>, you could then retrieve the count for any DATE using the consistent format of yyyy-MM-dd. And the process would need no casts to work.

Map<String, Integer> perDay = new HashMap<>();
perDay.put("2022-01-01", 5);
perDay.put("2022-01-02", 3);
perDay.put("2022-02-29", 4);

This also makes it easier to convert to the month total sales as follows:

  • stream the entrySets for perDay
  • convert to a Map<String,Integer> for month and sales. Use substring to get just the year and month portion via Entry.getKey(). Note: this works only if your date format is consistent.
  • get the count from Entry::getValue using a method reference.
  • and for duplicate counts (same month, different days) sum them using Integer::sum

Map<String, Integer> monthSales = daySales.entrySet()
        .stream().collect(Collectors.toMap(e->e.getKey().substring(0,7),
                             Entry::getValue,
                             Integer::sum));

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

prints

2022-01=8
2022-02=4

And like before, you can get the monthly counts for any yyyy-MM key if it exists.

Suggestions:

  • first and foremost, don't use Object. Use a specific type, in your case Integer for the value. Otherwise you will have to do casting which can lead to errors.

  • Consider using a class to Store related information. A sales class could be used to store the Date and count as well as other information.

     class Sales {
         private int count; 
         private String date;
         // getters
         // toString
    }
    
  • For handling dates, look at the classes in the java.time package.

Upvotes: 1

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 29028

The approach you've introduced is not maintainable and as a consequence prone to errors and code duplications, and also makes extension of the code harder.

These are the main issues:

  • Don't use Object as a generic type. Because a collection element of type Object worth nothing without casting. If you have no intention to write muddy code full of casts and intanceof checks, don't take this rode.

  • Map - is not the same thing as JSON-object (I'm not talking about classes from Java-libraries like Gson, they are map-based), don't confuse a way of structuring the human-readable text with the implementation of the data structure.

  • Keys like "DATE" and "COUNT" are useless - you can simply store the actual date and count instead.

  • Using String as a type for numbers and dates doesn't give you any advantage. There's nothing you can do with them (apart from comparing and printing on the console) without parsing, almost like with Object. Use an appropriate data type for every element. Why? Because LocalDate, Integer, YearMonth have their own unique behavior that String can't offer you.

That's how you can improve your code by tuning the collection representing sales per day List<Map<String, Object>> into a Map<LocalDate, Integer> which is way more handy:

Map<LocalDate, Integer> salesPerDay = perDay.stream()
    .map(map -> Map.entry(LocalDate.parse((String) map.get("DATE"), DateTimeFormatter.ISO_DATE),
                          Integer.parseInt((String) map.get("COUNT"))))
    .collect(Collectors.groupingBy(
                Map.Entry::getKey,
                Collectors.summingInt(Map.Entry::getValue)));

And that's how you can produce a map containing sales per month from it (YearMonth from the java.time package is used as a key):

Map<YearMonth, Integer> salesPerMonth = salesPerDay.entrySet().stream()
    .collect(Collectors.groupingBy(
                entry -> YearMonth.from(entry.getKey()),
                Collectors.summingInt(Map.Entry::getValue)));

salesPerMonth.forEach((k, v) -> System.out.println(k + " -> " + v));

Output:

2022-01 -> 8
2022-02 -> 4

A link to Online Demo

Upvotes: 2

Krzysztof Strzała
Krzysztof Strzała

Reputation: 94

assuming that your dates are always in format

2022-01-01

You could try:

 var map = perDay.stream().collect(
                  Collectors.groupingBy(monthMap -> ((String) monthMap.get("DATE")).substring(0, 7),
                  Collectors.summingInt(monthMap -> Integer.parseInt(String.valueOf(monthMap.get("COUNT"))))));

Returns:

{2022-01=8, 2022-02=4}

Upvotes: 2

Related Questions