I just started with learning and implementing collections via the Java 8 stream API. I have one class:
public class Discount {
int amount;
String lastMarketingRegion;
Discount (int amount, String lastMarketingRegion) {
this.amount = amount;
this.lastMarketingRegion= lastMarketingRegion;
}
public int getAmount() { return amount; }
public String getLastMarketingRegion() { return lastMarketingRegion; }
public String toString() {
return String.format("{%s,\"%s\"}", amount, lastMarketingRegion);
}
}
And I am given with the following:
Map<String, Discount> prepaid = new HashMap<String, Discount>();
prepaid.put("HAPPY50", new Discount(100, "M1"));
prepaid.put("LUCKY10", new Discount(10, "M2"));
prepaid.put("FIRSTPAY", new Discount(20, "M3"));
Map<String, Discount> otherBills = new HashMap<String, Discount>();
otherBills.put("HAPPY50", new Discount(60, "M4"));
otherBills.put("LUCKY10", new Discount(7, "M5"));
otherBills.put("GOOD", new Discount(20, "M6"));
List<Map<String, Discount>> discList = new ArrayList<Map<String, Discount>>();
discList.add(prepaid);
discList.add(otherBills);
So, basically I have a list of Discount maps of all discount codes for different payment types.
Requirement is to create a single map with all the discount codes across all payment types with sum_of_amount and the last_region:
Map<String, Discount> totalDiscounts =
{LUCKY10={17, "M5"}, FIRSTPAY={20, "M3"}, HAPPY50={160, "M4"}, GOOD={20, "M6"}}
I am able to get:
Map<String, Integer> totalDiscounts =
{LUCKY10=17, FIRSTPAY=20, HAPPY50=160, GOOD=20}
by using the following code:
Map<String, Integer> afterFormatting = discList.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.summingInt(map -> map.getValue().amount)));
but I need a Discount object also with the region.
I need a collection of Discount objects where the amount is the total of the amounts of same key and region is from otherBills.
Any help would be much appreciated. Thank You.
Edit 1 -
For the sake of simplicity, please consider lastMarketingRegion to have same value for a discount code.
I also tried to explain it via diagram -
From comments
Why do you expect "LUCKY10" - "M5" when you have "M2" and "M5" entries for LUCKY10?
because otherBills has more priority than prepaid
You can use Collectors.toMap for this. The last argument to it is the mergeFunction that merges two Discounts that had same String key in the map.
Map<String, Discount> totalDiscounts = discList.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(discount1, discount2) -> new Discount(discount1.getAmount() + discount2.getAmount(),
discount2.getLastMarketingRegion())));
Since the stream generated out of a list is ordered, the discount2 Discount will be the one from the otherBills map and hence I'm picking the region of it.
If you have constructed the list by adding otherBills followed by prepaid, then this will have a different output.
Relying on the encounter order makes this a not-a-great-solution.
(If you are going to assume we process entries from the second map after processing the first, why merge them in the first place?)
See my other answer that uses Map.merge
If you have just two maps, then rather than going for a stream-based solution (my other answer), you can use Map.merge for this.
Here, we make a copy of the prepaid map. Then we iterate through the otherBills map. For each key
If the mapping does not exist, it adds it to the map (result map)
If the mapping already exists, we construct a new Discount object whose amount is the sum of amounts of the Discount object already present in the map (the one from prepaid) and the current Discount object (the one from otherBill). It takes the region of the Discount object from the otherBill map.
Map<String, Discount> result = new HashMap<>(prepaid);
otherBills.forEach((k, v) -> result.merge(k, v, (discountFromPrepaid, discountFromOtherBill) ->
new Discount(discountFromPrepaid.getAmount() + discountFromOtherBill.getAmount(),
discountFromOtherBill.getLastMarketingRegion())));
Related
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?
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}
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
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.
I am pretty new to java moving from c#. I have the following class.
class Resource {
String name;
String category;
String component;
String group;
}
I want to know the following numbers:
1. Count of resources in the category.
2. Distinct count of components in each category. (component names can be duplicate)
3. Count of resources grouped by category and group.
I was able to achieve a little bit of success using Collectors.groupingBy. However, the result is always like this.
Map<String, List<Resource>>
To get the counts I have to parse the keyset and compute the sizes.
Using c# linq, I can easily compute all the above metrics.
I am assuming there is definitely a better way to do this in java as well. Please advise.
For #1, I'd use Collectors.groupingBy along with Collectors.counting:
Map<String, Long> resourcesByCategoryCount = resources.stream()
.collect(Collectors.groupingBy(
Resource::getCategory,
Collectors.counting()));
This groups Resource elements by category, counting how many of them belong to each category.
For #2, I wouldn't use streams. Instead, I'd use the Map.computeIfAbsent operation (introduced in Java 8):
Map<String, Set<String>> distinctComponentsByCategory = new LinkedHashMap<>();
resources.forEach(r -> distinctComponentsByCategory.computeIfAbsent(
r.getCategory(),
k -> new HashSet<>())
.add(r.getGroup()));
This first creates a LinkedHashMap (which preserves insertion order). Then, Resource elements are iterated and put into this map in such a way that they are grouped by category and each group is added to a HashSet that is mapped to each category. As sets don't allow duplicates, there won't be duplicated groups for any category. Then, the distinct count of groups is the size of each set.
For #3, I'd again use Collectors.groupingBy along with Collectors.counting, but I'd use a composite key to group by:
Map<List<String>, Long> resourcesByCategoryAndGroup = resources.stream()
.collect(Collectors.groupingBy(
r -> Arrays.asList(r.getCategory(), r.getGroup()), // or List.of
Collectors.counting()));
This groups Resource elements by category and group, counting how many of them belong to each (category, group) pair. For the grouping key, a two-element List<String> is being used, with the category being its 1st element and the component being its 2nd element.
Or, instead of using a composite key, you could use nested grouping:
Map<String, Map<String, Long>> resourcesByCategoryAndGroup = resources.stream()
.collect(Collectors.groupingBy(
Resource::getCategory,
Collectors.groupingBy(
Resource::getGroup,
Collectors.counting())));
Thanks Fedrico for detailed response. #1 and #3 worked great. For #2, i would like to see an output of Map. Here's the code that i am using currently to get that count. This is without using collectors in old style.
HashMap<String, HashSet<String>> map = new HashMap<>();
for (Resource resource : resources) {
if (map.containsKey(resource.getCategory())) {
map.get(resource.getCategory()).add(resource.getGroup());
} else
HashSet<String> componentSet = new HashSet<>();
componentSet.add(resource.getGroup());
map.put(resource.getCategory(), componentSet);
}
}
log.info("Group count in each category");
for (Map.Entry<String, HashSet<String>> entry : map.entrySet()) {
log.info("{} - {}", entry.getKey(), entry.getValue().size());
}
I have a structure as shown below. I am trying to calculate the sum of all employee salaries. I would like to use Java streams. Could someone please explain how I can achieve this?
Employee has a getSalary method.
Map<String, Map<String, Employee>> mainMap = new HashMap<>();
Map<String, Employee> emplmap1 = new HashMap<>();
Map<String, Employee> emplmap2 = new HashMap<>();
emplmap1.put("A",empl1);
emplmap1.put("B",empl2);
You want to use flatMap, which can accept an element that's a stream and then flattens all the streams into one stream, and a map which converts elements in a stream to different elements:
double salary = mainMap.values().stream()
.flatMap(m -> m.values().stream())
.map(Employee::getSalary)
.mapToDouble(Double::doubleValue)
.sum();
System.out.println("Total salary: " + salary);
mainMap.values().stream() will return a stream of mainMap's values (the maps). We flatMap the stream by turning every element (a map) to a stream of that map's values. Then we get the salaries, turn them into primitive doubles and finally, we sum them.
You might want to consider using the built in stats collector if you will possibly need other stats in the future.
DoubleSummaryStatistics salaryStats = mainMap.values().stream()
.flatMap(m -> m.values().stream())
.collect(Collectors.summarizingDouble(Employee::getSalary));
That way you get sum, count, max, min, average.
Using java stream, how to create a Map from a List to index by 2 keys on the same class?
I give here a code Example, I would like the map "personByName" to get all person by firstName OR lastName, so I would like to get the 3 "steves": when it's their firstName or lastname. I don't know how to mix the 2 Collectors.groupingBy.
public static class Person {
final String firstName;
final String lastName;
protected Person(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
#Test
public void testStream() {
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
Map<String, Set<Person>> personByFirstName = persons.stream().collect(Collectors.groupingBy(Person::getFirstName, Collectors.toSet()));
Map<String, Set<Person>> personByLastName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));
Map<String, Set<Person>> personByName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));// This is wrong, I want bot first and last name
Assert.assertEquals("we should search by firstName AND lastName", 3, personByName.get("Steve").size()); // This fails
}
I found a workaround by looping on the 2 maps, but it is not stream-oriented.
You can do it like this:
Map<String, Set<Person>> personByName = persons.stream()
.flatMap(p -> Stream.of(new SimpleEntry<>(p.getFirstName(), p),
new SimpleEntry<>(p.getLastName(), p)))
.collect(Collectors.groupingBy(SimpleEntry::getKey,
Collectors.mapping(SimpleEntry::getValue, Collectors.toSet())));
Assuming you add a toString() method to the Person class, you can then see result using:
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
// code above here
personByName.entrySet().forEach(System.out::println);
Output
Steve=[Steve Wozniac, Bill Steve, Steve Jobs]
Jobs=[Steve Jobs]
Bill=[Bill Steve, Bill Gates]
Wozniac=[Steve Wozniac]
Gates=[Bill Gates]
You could merge the two Map<String, Set<Person>> for example
Map<String, Set<Person>> personByFirstName =
persons.stream()
.collect(Collectors.groupingBy(
Person::getFirstName,
Collectors.toCollection(HashSet::new))
);
persons.stream()
.collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()))
.forEach((str, set) -> personByFirstName.merge(str, set, (s1, s2) -> {
s1.addAll(s2);
return s1;
}));
// personByFirstName contains now all personByName
One way would be by using the newest JDK12's Collector.teeing:
Map<String, List<Person>> result = persons.stream()
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(ArrayList::new)),
Collectors.groupingBy(Person::getLastName),
(byFirst, byLast) -> {
byLast.forEach((last, peopleList) ->
byFirst.computeIfAbsent(last, k -> new ArrayList<>())
.addAll(peopleList));
return byFirst;
}));
Collectors.teeing collects to two separate collectors and then merges the results into a final value. From the docs:
Returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.
So, the above code collects to a map by first name and also to a map by last name and then merges both maps into a final map by iterating the byLast map and merging each one of its entries into the byFirst map by means of the Map.computeIfAbsent method. Finally, the byFirst map is returned.
Note that I've collected to a Map<String, List<Person>> instead of to a Map<String, Set<Person>> to keep the example simple. If you actually need a map of sets, you could do it as follows:
Map<String, Set<Person>> result = persons.stream().
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(LinkedHashSet::new)),
Collectors.groupingBy(Person::getLastName, Collectors.toSet()),
(byFirst, byLast) -> {
byLast.forEach((last, peopleSet) ->
byFirst.computeIfAbsent(last, k -> new LinkedHashSet<>())
.addAll(peopleSet));
return byFirst;
}));
Keep in mind that if you need to have Set<Person> as the values of the maps, the Person class must implement the hashCode and equals methods consistently.
If you want a real stream-oriented solution, make sure you don't produce any large intermediate collections, else most of the sense of streams is lost.
If just you want to just filter all Steves, filter first, collect later:
persons.stream
.filter(p -> p.getFirstName().equals('Steve') || p.getLastName.equals('Steve'))
.collect(toList());
If you want to do complex things with a stream element, e.g. put an element into multiple collections, or in a map under several keys, just consume a stream using forEach, and write inside it whatever handling logic you want.
You cannot key your maps by multiple values. For what you want to achieve, you have three options:
Combine your "personByFirstName" and "personByLastName" maps, you will have duplicate values (eg. Bill Gates will be in the map under the key Bill and also in the map under the key Gates). #Andreas answer gives a good stream-based way to do this.
Use an indexing library like lucene and index all your Person objects by first name and last name.
The stream approach - it will not be performant on large data sets but you can stream your collection and use filter to get your matches:
persons
.stream()
.filter(p -> p.getFirstName().equals("Steve")
|| p.getLastName().equals("Steve"))
.collect(Collectors.asList());
(I've written the syntax from memory so you might have to tweak it).
If I got it right you want to map each Person twice, once for the first name and once for the last.
To do this you have to double your stream somehow. Assuming Couple is some existing 2-tuple (Guava or Vavr have some nice implementation) you could:
persons.stream()
.map(p -> new Couple(new Couple(p.firstName, p), new Couple(p.lastName, p)))
.flatMap(c -> Stream.of(c.left, c.right)) // Stream of Couple(String, Person)
.map(c -> new Couple(c.left, Arrays.asList(c.right)))
.collect(Collectors.toMap(Couple::getLeft, Couple::getRight, Collection::addAll));
I didn't test it, but the concept is: make a stream of (name, person), (surname, person)... for every person, then simply map for the left value of each couple. The asList is to have a collection as value. If you need a Set chenge the last line with .collect(Collectors.toMap(Couple::getLeft, c -> new HashSet(c.getRight), Collection::addAll))
Try SetMultimap, either from Google Guava or my library abacus-common
SetMultimap<String, Person> result = Multimaps.newSetMultimap(new HashMap<>(), () -> new HashSet<>()); // by Google Guava.
// Or result = N.newSetMultimap(); // By Abacus-Util
persons.forEach(p -> {
result.put(p.getFirstName(), p);
result.put(p.getLastName(), p);
});
What would be the simplest way to merge Map key values like keys "55", "55004", "550009", "550012" into one key: "55" and a sum of all those values().
I'm trying to think of ways to use containsKey or trimming the key. It's very hard to think about this.
Maybe a flatMap to flatten the map and reduce.
#Test
public void TestM(){
Map<String,Object> map1 = new HashMap();
map1.put("55", 3453.34);
map1.put("55001", 5322.44);
map1.put("55003", 10112.44);
map1.put("55004", 15555.74);
map1.put("77", 1000.74); // instead of 1000 it should be ~1500
map1.put("77004", 444.74);
map1.put("77003", 66.74);
// in real example I'll need "77" and "88" and "101" etc.
// All of which has little pieces like 77004, 77006
Map<String,Double> SumMap = new HashMap<String, Double>();
SumMap = map1.entrySet().stream().map
(e->e.getValue()).reduce(0d, Double::sum);
// INCORRECT
// REDUCE INTO ONE KEY startsWith 55
System.out.println("Map: " + SumMap);
// RESULT should be :
// Map<String, Double> result = { "55": TOTAL }
// real example might be "77": TOTAL, "88": TOTAL, "101": TOTAL
//(reducing away the "77004", "88005" etc.)
}
Basically this code reduces and rolls subitem totals into a bigger key.
It looks like you could use Collectors.groupingBy.
It requires Function which would allow us decide which elements belong to same group. Such function for elements from same group should always return same value which will be used as key in resulting map. In your case it looks like you want to group elements with same first two characters stored in key, which suggest mapping to substring(0,2).
When we already have way to determine which elements belong to same group, we can now specify how we want map to collect them. By default it collects them in list so we have key->[elemnt0, element1, ...] mapping.
But we can specify your own way of handling elements from same group by providing our own Collector. Since we want to create sum of values we can use Collectors.summingDouble(mappingToDouble).
DEMO:
Map<String, Double> map1 = new HashMap<>();
map1.put("661", 123d);
map1.put("662", 321d);
map1.put("55", 3453.34);
map1.put("55001", 5322.44);
map1.put("55003", 10112.44);
map1.put("55004", 15555.74);
Map<String, Double> map = map1.entrySet()
.stream()
.collect(
Collectors.groupingBy(
entry -> entry.getKey().substring(0, 2),
Collectors.summingDouble(Map.Entry::getValue)
)
);
System.out.println(map);
Output: {66=444.0, 55=34443.96}