Given the following class Test
class Test {
String testName;
String studName;
String status;
}
and a list of tests
List<Test> tests = List.of(
new Test("English", "John", "passed"),
new Test("English", "Dave", "passed"),
new Test("Science", "Alex", "failed"),
new Test("Science", "Jane", "failed"),
new Test("History", "Dave", "passed"),
new Test("Mathematics", "Anna", "passed"),
new Test("Mathematics", "Lisa", "passed"),
new Test("Mathematics", "Paul", "failed"),
new Test("Geography", "Mark", "passed"),
new Test("Physics", "John", "failed"));
I need to group by testName and count only where status equals "passed". I need to do the equivalent of below code with streams :
Map<String, Long> result2 = new HashMap<>();
for (Test t : tests) {
result2.putIfAbsent(t.getTestName(), 0L);
if (t.getStatus().equals("passed")) {
result2.computeIfPresent(t.getTestName(), (k, v) -> v + 1);
}
}
The correct and desired output:
{Geography=1, English=2, Science=0, Mathematics=2, History=1, Physics=0}
I'm looking for a stream approach, but couldn't find a solution yet. A simple Collectors.counting will count all, regardless of status "failed/passed":
Map<String, Long> resultCounting = tests.stream()
.collect(Collectors.groupingBy(
Test::getTestName,
Collectors.counting()
));
Output:
{Geography=1, English=2, Science=2, Mathematics=3, History=1, Physics=1}
I thought about filtering beforehand, but then I will loose those subjects where all statuses are "failed".
Map<String, Long> resultFilter = tests.stream()
.filter(t -> t.getStatus().equals("passed"))
.collect(Collectors.groupingBy(
Test::getTestName,
Collectors.counting()
));
Output:
{Geography=1, English=2, Mathematics=2, History=1}
How can I group all tests by testName, but count only those where status is "passed" ?
Is it possible to wrap Collectors.counting() in some kind of condition?
You can achieve the desired result by using collector toMap(keyMapper,valueMapper,mergeFunction).
valueMapper function would either produce 1 or 0 depending on on the status.
Map<String, Integer> passCountByTestName = tests.stream()
.collect(Collectors.toMap(
Test::getTestName,
test -> test.getStatus().equals("passed") ? 1 : 0,
Integer::sum
));
passCountByTestName.forEach((k, v) -> System.out.println(k + " -> " + v));
Output:
Geography -> 1
English -> 2
Science -> 0
Mathematics -> 2
History -> 1
Physics -> 0
Sidenote: it would be better to use boolean or enum as type for the status property instead of relying on string values.
Related
lets say I have:
bob:V
bob:A
bob:B
bob:C
bob:C
sally:B
sally:C
sally:A
steve:A
steve:B
steve:C
how do I store:
the values as:
bob={V,A,B,C,C}, sally={B,C,A}, steve={A,B,C}
and for any guy who has a sequence A,B,C repeated how do I get that person name?
I am fairly new to Java and Im trying to implement this scenario, as I dont see anything like this in this communtiy.
here is my final answer: first stored the list into a map and then used collectors to loop through and map it to their respective attributes.
public class Solution{
static List<String> doWork(List<LogItem> eventsInput) {
Map<String, String> personMap = eventsInput.stream()
.collect(Collectors.toMap(LogItem::getUserId, p -> Character.toString(p.getEventChar()), String::concat));
System.out.println("person map is \n" + personMap);
BiPredicate<Entry<String, List<String>>, String> contains =
(entry, attr) -> entry.getValue().stream()
.collect(Collectors.joining()).contains(attr);
String attributes = "ABC";
List<String> results = personMap.entrySet().stream()
.filter(e -> e.getValue().contains(attributes))
.map(Entry::getKey).collect(Collectors.toList());
return results;
}
public static void main(String[] args){
List<LogItem> exampleInputItems = new ArrayList<>();
exampleInputItems.add(new LogItem("bob", 'V'));
exampleInputItems.add(new LogItem("bob", 'A'));
exampleInputItems.add(new LogItem("steve", 'A'));
exampleInputItems.add(new LogItem("bob", 'B'));
exampleInputItems.add(new LogItem("bob", 'C'));
exampleInputItems.add(new LogItem("bob", 'C'));
exampleInputItems.add(new LogItem("steve", 'B'));
exampleInputItems.add(new LogItem("sally", 'B'));
exampleInputItems.add(new LogItem("steve", 'C'));
exampleInputItems.add(new LogItem("sally", 'C'));
exampleInputItems.add(new LogItem("sally", 'A'));
List<String> returnedNames = doWork(exampleInputItems);
if (returnedNames.size() != 2) {
throw new RuntimeException("Wrong number of names found. Found: " + returnedNames);
}
if (!returnedNames.contains("bob")) {
throw new RuntimeException("Did not find \"bob\" in the returnedNames: " + returnedNames);
}
if (!returnedNames.contains("steve")) {
throw new RuntimeException("Did not find \"steve\" in the returnedNames: " + returnedNames);
}
System.out.println("The example passed.");
}
static class LogItem {
public String userId;
public char eventChar;
public LocalDateTime dateTime;
LogItem(String userId, char eventChar) {
this.userId = userId;
this.eventChar = eventChar;
dateTime = LocalDateTime.now();
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public char getEventChar() {
return eventChar;
}
public void setEventChar(char eventChar) {
this.eventChar = eventChar;
}
public LocalDateTime getDateTime() {
return dateTime;
}
public void setDateTime(LocalDateTime dateTime) {
this.dateTime = dateTime;
}
#Override
public String toString() {
return "LogItem [userId=" + userId + ", eventChar=" + eventChar + ", dateTime=" + dateTime + ", getUserId()="
+ getUserId() + ", getEventChar()=" + getEventChar() + ", getDateTime()=" + getDateTime() + "]";
}
}
}
}
First, I would store the attributes in a Map<String,String>. This will make it easier to filter the attributes later. I am using a record in lieu of a class but a class would work as well.
record Person(String getName, String getAttribute) {
}
Create the list of Person objects
List<Person> list = List.of(new Person("bob", "V"),
new Person("bob", "A"), new Person("bob", "B"),
new Person("bob", "C"), new Person("bob", "C"),
new Person("sally", "B"), new Person("sally", "C"),
new Person("sally", "A"), new Person("steve", "A"),
new Person("steve", "B"), new Person("steve", "C"));
Now create the map. Simply stream the list of people and concatenate the attributes for each person.
Map<String, String> personMap = list.stream()
.collect(Collectors.toMap(Person::getName,
Person::getAttribute, String::concat));
The map will look like this.
bob=VABCC
steve=ABC
sally=BCA
Now grab the name based on an attribute string.
Now stream the entries of the map and pass the entry whose value contains the attribute string. Then retrieve the key (name) and return as a list of names.
String attributes = "ABC";
ListString> results = personMap.entrySet().stream()
.filter(e -> e.getValue().contains(attributes))
.map(Entry::getKey).collect(Collectors.toList());
System.out.println(results);
prints
[bob, steve]
Alternative approach using Map<String, List<String>>
Group the objects by name but the values will be a list of attributes instead of a string.
Map<String, List<String>> personMap = list.stream()
.collect(Collectors.groupingBy(Person::getName,
Collectors.mapping(Person::getAttribute,
Collectors.toList())));
The map will look like this.
bob=[V, A, B, C, C]
steve=[A, B, C]
sally=[B, C, A]
To facilitate testing the attributes, a BiPredicate is used to stream the list and concatinate the attributes and then check and see if it contains the attribute string.
BiPredicate<Entry<String, List<String>>, String> contains =
(entry, attr) -> entry.getValue().stream()
.collect(Collectors.joining()).contains(attr);
As before, stream the entry set of the map and apply the filter to pass those entries which satisfy the condition. In this case, the filter invokes the BiPredicate.
String attributes = "ABC";
List<String> results = personMap.entrySet().stream()
.filter(e->contains.test(e, attributes))
.map(Entry::getKey).collect(Collectors.toList());
System.out.println(results);
prints
[bob, steve]
Update Answer
To work with Character attributes, you can do the following using the first example.
Map<String, String> personMap2 = list.stream()
.collect(Collectors.toMap(Person::getName,
p -> Character.toString(p.getAttribute()),
String::concat));
Imo, it would be easier, if possible to change your attribute types to string.
Edited version! From a PC instead of my phone.
I have a Class defined with following attributes:
Here is the code I have for a sample Map without being part of another class:
List<Map<String,Long>> amountList = new ArrayList<>();
Map<String, Long> amountMap = new HashMap<>();
for(int i=0; i<2;i++ ) {
amountMap.put("AMOUNT1", 12L);
amountMap.put("AMOUNT2", 10L);
amountMap.put("AMOUNT3", 10L);
amountMap.put("AMOUNT4", 12L);
amountMap.put("AMOUNT5", 10L);
amountList.add(amountMap);
}
Map<String, Long> collectset = amountList.stream()
.flatMap(entry -> entry.entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue, Long::sum));
for (String str : collectset.keySet()){
System.out.println( "output: " + str + " -> " + collectset.get(str));
}
I need a result from this where the output is just as below:
output: AMOUNT3 -> 20
output: AMOUNT2 -> 20
output: AMOUNT1 -> 24
output: AMOUNT5 -> 20
output: AMOUNT4 -> 24
What I get as a result of the code above is that the values are repeating twice.
Is there a way to only output the Sum equivalent once. For instance, if the loop is changed to produce 5 Maps - I see the output printed 5 times.
Make an info object containing three strings and use that as your key value (don't forget to override hashCode if needed). Or simply use a format (such as CSV) to concatinate your strings together and then use that string as a key.
I was able to find the issue. There was a for loop just before the Stream implementation which caused to print the output based on how many times I was looping through the loop.
Here is the updated code:
List<Map<String,Long>> countList = new ArrayList<>();
Map<String, Long> countMap = new HashMap<>();
Random random = new Random();
for(int i=0; i<500;i++ ) {
countMap.put("COUNT" + random.nextInt(10), 12L);
countMap.put("COUNT" + random.nextInt(10), 10L);
countMap.put("COUNT" + random.nextInt(10), 10L);
countMap.put("COUNT" + random.nextInt(10), 12L);
countMap.put("COUNT" + random.nextInt(10), 10L);
countList.add(countMap);
}
Map<String, Long> collectset = countList.stream()
.flatMap(entry -> entry.entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue, Long::sum));
System.out.println( "CollectSet Size: " + collectset.size());
I'm creating a pipeline in Java 8 that is able to take a list of students and the name of their clubs that they joined. I want to generate a list of students that joined multiple clubs, so basically I need to return the duplicate names of students that joined 2 or more clubs.
This is my data.txt file where the pipeline is getting the required data.
ARTS:Joey Solydan:Economics
MUSIC:Joey Solydan:Economics
ARTS:Haley Wolloims:WomenStudies
SPORTS:Godfriey Lemonsquesser:Cookery
LITERATURECLUB:Say Moniki:Archeology
FILM:Milles Spielberg:Masscom
ARTS:Milles Spielberg:Masscom
Basically, I need the code to return the names of Joey Solydan and Milles Spielberg because they are in 2 clubs unlike the other names.
So the next step is to create a method to look for Joey and Milles using Java 8 lambda and I started off the method as such:
public static void displayDuplicateNames(ArrayList<Member> member) {
Map<String, Map<String, Set<Member>>> allMembers = new TreeMap<>();
//For loop to initialize the Map
for (Member members : member) {
String club = members.getClub();
String name = members.getName();
Map<String, Set<Member>> clubList = allMembers.computeIfAbsent(org, k -> new TreeMap<>());
Set<Member> nameList = clubList.computeIfAbsent(name, k -> new TreeSet<>());
nameList.add(members);
}
next is to create the pipeline and it follows just after the for loop:
allMembers
.forEach(
(club, clubList) -> {
System.out.printf("\n*** Club %s:", club);
clubList
.forEach(
(name, nameList) ->{
System.out.printf("\n** Member's Name with Multiple Clubs: %s\n", name);
// I got stumped on what to do next
}
);
});
I want an expected output of something like:
*** Club Arts:
** Member's Name with Multiple Clubs: Joey Solydan
** Member's Name with Multiple Clubs: Milles Spielberg
*** Club Music:
** Member's Name with Multiple Clubs: Joey Solydan
*** Club Film:
** Member's Name with Multiple Clubs: Milles Spielberg
UPDATE:
I've followed the code segment from WJS and the current code looks like this:
public static List<String> displayDuplicateNames(ArrayList<Member> member) {
Map<String, Integer> dups = new HashMap<>();
Map<String, Map<String, Set<Member>>> allMembers = new TreeMap<>();
for (Member members : member) {
String org = members.getOrg();
String name = members.getName();
Map<String, Set<Member>> orgList = allMembers.computeIfAbsent(org, k -> new TreeMap<>());
Set<Member> nameList = orgList.computeIfAbsent(name, k -> new TreeSet<>());
nameList.add(members);
}
allMembers
.forEach(
(org, orgList) -> {
System.out.printf("\n*** Organization %s: ", org);
orgList
.forEach(
(name, nameList) ->{
System.out.printf("\n** Member's Name with Multiple Organizations: %s\n", name);
if (dups.containsKey(name)) {
dups.put(name, dups.get(name) + 1);
}
else {
dups.put(name, 1);
}
}
);
});
return dups.entrySet().stream().filter(e -> e.getValue() > 1).map(
Map.Entry::getKey).collect(Collectors.toList());
}
My method for reading the data from the file has been updated as well:
public static List<Member> readDataFromFile(String filename)
{
String line = "";
ArrayList<Member> memberList = new ArrayList<>();
try {
Scanner scanner = new Scanner(new File(filename));
while (scanner.hasNextLine()) {
line = scanner.nextLine();
String[] parts = line.split("[:]");
String club = parts[1];
String name = parts[2];
String course = parts[0];
memberList.add(new Member(club,name,course));
}
} catch (FileNotFoundException ex) {
System.out.println("ERROR, FILE NOT FOUND.");
}
return memberList;
}
The current output is now:
It shows the members of each club but now I need to just show each club with a member that is joined with another club other than theirs.
Here is one possibility.
Put this at the top of the dups method.
Map<String, Integer> dups = new HashMap<>();
Add the following code at your comment
if (dups.containsKey(name)) {
dups.put(name, dups.get(name) + 1);
}
else {
dups.put(name, 1);
}
and return this at the end of the method.
return dups.entrySet().stream().filter(e -> e.getValue() > 1).map(
Entry::getKey).collect(Collectors.toList());
Finally, make certain your displayDups method returns a List<String>
Here is your modified method.
public static List<String> displayDuplicateNames(List<Member> members) {
Map<String, Map<String, Set<Member>>> allMembers = new TreeMap<>();
Map<String, Integer> dups = new HashMap<>();
// For loop to initialize the Map
for (Member member : members) {
String club = member.getClub();
String name = member.getName();
Map<String, Set<Member>> clubList =
allMembers.computeIfAbsent(club, k -> new TreeMap<>());
Set<Member> nameList =
clubList.computeIfAbsent(name, k -> new TreeSet<>());
nameList.add(member);
}
allMembers.forEach((club, clubList) ->
{
System.out.printf("%n*** Club %s:%n", club);
clubList.forEach((name, nameList) ->
{
System.out.printf(" ** Member's Name with Multiple Clubs:
%s%n",
name);
if (dups.containsKey(name)) {
dups.put(name, dups.get(name) + 1);
}
else {
dups.put(name, 1);
}
});
});
return dups.entrySet().stream().filter(e -> e.getValue() > 1).map(
Entry::getKey).collect(Collectors.toList());
}
}
It returns
dups = [Joey Solydan, Milles Spielberg]
Note that I had to make my own Member class since it was not included in your question. So I added getters to retrieve the values.
If you don't mind processing you input twice you could first count how often your students enrolled. Assuming you have all your students in the list memberList:
Map<string, Long> counted = memberList.stream()
.map(member -> member.getName())
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
Now counted contains all names once with a count of how often they came up.
Next you could take your memberList and do
stream().filter(member -> counted[member.name] > 1)
That (after collecting yada yada) should result in a list which only contains Members enrolled in more than one club.
Hope the syntax is correct. Didn't code Java for a while now :-)
I want to return JSON from Rest API endpoint as keys with values. Example:
{
"terminal 1":
{"date":"2018-10-06T00:00:00.000+0000","volume":111,"count":1},
"terminal 2":
{"date":"2018-11-06T00:00:00.000+0000","volume":122,"count":1}
}
How I can add the keys? It should be I suppose like this:
List<String<List<TopTerminalsDTO>>>>
Can you give me some code example?
Latest attempt to clean the final code:
#GetMapping("/terminals")
public ResponseEntity<Map<Integer, List<TopTerminalsDTO>>> getTopTerminalsVolumes(
#RequestParam(value = "start_date", required = true) String start_date,
#RequestParam(value = "end_date", required = true) String end_date) {
LocalDateTime start_datel = LocalDateTime.now(Clock.systemUTC());
LocalDateTime end_datel = LocalDateTime.now(Clock.systemUTC());
final List<PaymentTransactionsDailyFacts> list = dashboardRepository.top_daily_transactions(start_datel, end_datel);
final Collector<PaymentTransactionsDailyFacts, List<TopTerminalsDTO>, List<TopTerminalsDTO>> terminalsCollector =
Collector.of(
ArrayList::new,
(terminals, p) -> terminals.add(mapper.toTopTerminalsDTO(p)),
(accumulator, terminals) -> {
accumulator.addAll(terminals);
return accumulator;
}
);
final Map<Integer, List<TopTerminalsDTO>> final_map =
list.stream()
.filter(p -> p.getTerminal_id() != null)
.collect(Collectors.groupingBy(p -> p.getTerminal_id(), terminalsCollector));
return ResponseEntity.ok(final_map);
}
Following your JSON, testDate() should return Map<String, TopTerminalsDTO> instead of List.
Map<String, TopTerminalsDTO> result = newHashMap();
for (int i = 0; i <= 10; i++) {
TopTerminalsDTO ttDto = new TopTerminalsDTO();
ttDto.setCount(ThreadLocalRandom.current().nextInt(20, 500 + 1));
LocalDate localDate = LocalDate.now().minus(Period.ofDays((new Random().nextInt(365 * 70))));
Date date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
ttDto.setDate(date);
ttDto.setVolume(ThreadLocalRandom.current().nextInt(300, 5000 + 1));
result.put("terminal "+i, ttDto)
}
return result;
And, of course, change response type of rest method to ResponseEntity<Map<String, TopTerminalsDTO>>
This is what a Javascript dictionary looks like.
In Java, the correct representation is a Map<String, TopTerminalDto>.
Say you have an ordered List, and you want to return a Map with generated keys terminal{index}.
final List<TopTerminalDto> list = ...
final Map<String, TopTerminalDto> map =
IntStream.range(0, list.size())
.boxed()
.collect(toMap(i -> "terminal" + i, i -> list.get(i)));
The Spring endpoint would become:
#GetMapping("terminals")
Map<String, TopTerminalDto> getTopTerminalVolumes() { ... }
The ResponseEntity is not mandatory in Spring.
Remember to work as much as possible via Stream(s), to produce results without intermediate temporary state.
Additional example:
final List<PaymentTransactionsDailyFacts> list =
dashboardRepository.top_daily_transactions(start_datel, end_datel);
final Map<String, TopTerminalDto> map =
list.stream()
.collect(toMap(p -> p.getTerminal(), this::toDto))
// Conversion method
private TopTerminalDto toDto(final PaymentTransactionsDailyFacts p) {
// Implement conversion to Dto
}
For multiple values associated with a terminal:
final Map<Integer, List<TopTerminalDto>> map =
list.stream()
.filter(p -> p.getTerminal() != null)
.collect(groupingBy(
p -> p.getTerminal(),
Collector.of(
ArrayList::new,
(terminals, p) -> terminals.add(toDto(p)),
(accumulator, terminals) -> {
accumulator.addAll(terminals);
return accumulator;
}
)
));
You can clean the code by extracting the Collector.
final Collector<Integer, List<TopTerminalDto>, List<TopTerminalDto>> terminalsCollector =
Collector.of(
ArrayList::new,
(terminals, p) -> terminals.add(toDto(p)),
(accumulator, terminals) -> {
accumulator.addAll(terminals);
return accumulator;
}
)
final Map<Integer, List<TopTerminalDto>> map =
list.stream()
.filter(p -> p.getTerminal() != null)
.collect(groupingBy(p -> p.getTerminal(), terminalsCollector));
My output in Hashmap> is:
House cleaning = [Jack, Maria]
Computer lessons = [Leon, Maria]
Data recovery service = [Leon]
Computer repair = [Jack, Leon]
Handyman = [Jack]
Expected Output is
[[["Computer lessons"],["Leon","Maria"]],
[["Computer repair"],["Jack","Leon"]],
[["Data recovery service"],["Leon"]],
[["Handyman"],["Jack"]],
[["House cleaning"],["Jack","Maria"]]]
Order doesn't matter.
Try this.
Map<String, List<String>> map = new HashMap<>();
map.put("House cleaning", Arrays.asList("Jack", "Maria"));
map.put("Computer lessons", Arrays.asList("Leon", "Maria"));
map.put("Data recovery service", Arrays.asList("Leon"));
map.put("Computer repair", Arrays.asList("Jack", "Leon"));
map.put("Handyman", Arrays.asList("Jack"));
String[][][] result = map.entrySet().stream()
.map(e -> new String[][] {
new String[] {e.getKey()},
e.getValue().toArray(new String[0])})
.toArray(String[][][]::new);
for (String[][] row : result)
System.out.println(Arrays.deepToString(row));
result is
[[House cleaning], [Jack, Maria]]
[[Computer lessons], [Leon, Maria]]
[[Data recovery service], [Leon]]
[[Computer repair], [Jack, Leon]]
[[Handyman], [Jack]]