Reputation: 1651
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
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.
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:
entrySets
for perDay
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.Entry::getValue
using a method reference.
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
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
Upvotes: 2
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