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:
List<Map<String, Object>> perDay = new ArrayList<>();
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");
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?
CodePudding user response:
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}
CodePudding user response:
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 typeObject
worth nothing without casting. If you have no intention to write muddy code full of casts andintanceof
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 withObject
. Use an appropriate data type for every element. Why? BecauseLocalDate
,Integer
,YearMonth
have their own unique behavior thatString
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