Write two for instructions as a stream - java

I have two lists of Pearson (variables: FirstName, LastName, AllFirstName). One of them contains duplicates (if a pearson has two first names then in that list will have two entries for each name but the lastname will be the same) and one of them has only unique values (listWithUniquePearsons). The second list will be created by itereting over the first list and putting all the first name in a list of objects. I wrote this with two for. Is there any way to write it as a stream?
for (Pearson prs : listWithUniquePearsons) {
List<String> firstNames = new ArrayList<String>();
for (Pearson tempPerson : allPearsons) {
if (prs.getLastName().equals(tempPerson.getLastName())) {
firstNames.add(tempPerson.firstNames());
}
}
if (firstNames.size()>1) {
prs.setAllFirstNames(firstNames);
}
}

List<String> firstNames = listWithUniquePearsons
.stream()
.map(prs -> allPearsons.stream()
.filter(tempPerson -> prs.getLastName().equals(tempPerson.getLastName()))
.map(Person::getFirstName)
.collect(Collectors.toList());

You should build a map with a key lastName and values List<FirstName> and then remap its entries back to Pearson class setting allFirstNames. This can be done using Java 8 streams and collectors.
Let's assume that class Pearson is implemented as follows:
import java.util.*;
public class Pearson {
private String firstName;
private String lastName;
private List<String> allFirstNames;
public Pearson(String first, String last) {
this.firstName = first;
this.lastName = last;
}
public Pearson(List<String> allFirst, String last) {
this.allFirstNames = allFirst;
this.lastName = last;
}
public String getFirstName() {return firstName; }
public String getLastName() {return lastName; }
public List<String> getAllFirstNames() {return allFirstNames; }
}
Test code:
import java.util.*;
import java.util.stream.*;
public class Test {
public static void main(String[] args) {
List<Pearson> duplicates = Arrays.asList(
new Pearson("John", "Doe"),
new Pearson("William", "Doe"),
new Pearson("Edgar", "Poe"),
new Pearson("Allan", "Poe"),
new Pearson("Don", "King")
);
List<Pearson> uniques = duplicates.stream()
// map LastName -> List<FirstName>
.collect(Collectors.groupingBy(
Pearson::getLastName,
LinkedHashMap::new, // keep order of entries
Collectors.mapping(
Pearson::getFirstName,
Collectors.toList())
)).entrySet()
.stream()
.map(e -> new Pearson(e.getValue(), e.getKey()))
.collect(Collectors.toList());
uniques.forEach(u ->
System.out.println(
String.join(" ", u.getAllFirstNames())
+ " " + u.getLastName()
));
}
}
Output
John William Doe
Edgar Allan Poe
Don King

Related

Need to print last digit of string using lambda expression using java

I want to print the last digit from a string using a lambda expression. Using the below code I was able to print a complete number I but want to print the last digit
public static void main(String[] args) {
List<TestDTO> studs = new ArrayList<>();
studs.add(new TestDTO("101", "Test 101"));
studs.add(new TestDTO("102", "Test 102"));
Map<String, TestDTO> mapDbCardDtl = studs.stream().collect(Collectors.toMap(TestDTO::getId, Function.identity()));
Set<String> s = mapDbCardDtl.keySet();
System.out.println("s: " + s.toString());
}
Below is the DTO
public class TestDTO {
String id;
String name;
public TestDTO(String id, String name) {
super();
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Output from the above code:
s: [101, 102]
The expected output:
S : [1, 2]
You can't use Funcion#identity and expect to have different entity with modified values. One way is to convert Map<String, TestDTO> to Map<String, String> and use the following code:
Map<String, String> mapDbCardDtl = studs
.stream()
.collect(Collectors.toMap(TestDTO::getId,
(testDto) -> String.valueOf(testDto.getId().charAt(testDto.getId().length() - 1))));
Set<String> s = mapDbCardDtl.keySet();
System.out.println("s: " + s.toString());
If you are interested only in printing last number from the id, which is a number written as String, then:
List<String> s = studs.stream()
.map(dto->dto.getId())
.map(id -> String.valueOf(id.charAt(id.length() - 1))) // take last character and cast to String
.collect(Collectors.toList());
If you want to get. last digit from name value:
final Pattern numberPattern = Pattern.compile(".*([0-9]+).*$");
List<String> s = studs.stream()
// find nunmber in name
.map(dto -> numberPattern.matcher(dto.getName()))
.filter(Matcher::matches)
.map(matcher -> matcher.group(1))
// find last digit
.map(lastNumber ->String.valueOf(lastNumber.charAt(lastNumber.length()-1)))
.collect(Collectors.toList());
TIP:
If you wanted mapDbCardDtl to have last digit as the key, then you may fail, when more than one number ends with same digit. You will have to use overwrite merge function in toMap collector.
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
Second solution would be using groupBy method, that will aggregate TestDTO into Map<String,List< TestDTO >>. Here the key is your digit and value : list of Dto's with this digit.
If you simply want to print the last character of the keys, add this line right before the println statement:
s = s.stream().map(x -> x.substring(x.length() - 1)).collect(Collectors.toSet());
If you actually wanted the keys in the map to only be the last character, change the stream logic to:
Map<String, TestDTO> mapDbCardDtl = studs.stream()
.collect(Collectors.toMap(t -> t.getId().substring(t.getId().length() - 1), Function.identity()));
You can transform your Student's object into the last digit of id then collect in a list.
List<String> lastDigits =
studs.stream()
.map(s -> s.getId().substring(s.getId().length() - 1)))
.collect(Collectors.toList());
Note: If you collect in Set then it will contain only unique digits.
You have already done the most part of it, Just a little set manipulation was needed.
Look at the edit I made in the main method it will print your desire result.
public static void main(String[] args) {
List<TestDTO> studs = new ArrayList<>();
studs.add(new TestDTO("101", "Test 101"));
studs.add(new TestDTO("102", "Test 102"));
Map<String, TestDTO> mapDbCardDtl = studs.stream().collect(Collectors.toMap(TestDTO::getId, Function.identity()));
Set<String> s = mapDbCardDtl.keySet().stream()
.map(number -> String.valueOf(number.charAt(number.length() - 1))) // take last character and cast to String
.collect(Collectors.toSet());;
System.out.println("s: " + s);
}

Java Stream: How to change only first element of stream?

There is an ArrayList:
public void mainMethod() {
List<String> list = new ArrayList<>();
list.add("'+7913152','2020-05-25 00:00:25'");
list.add("'8912345','2020-05-25 00:01:49'");
list.add("'916952','2020-05-25 00:01:55'");
}
and method which transforms a string:
public String doTransform(String phone) {
String correctNumber;
..... make some changes...
return correctNumber;
}
list.stream().forEach(line -> {
Arrays.stream(line.split(","))... and what else?
How to take only first element of sub-stream (Arrays.stream) and pass it to transforming method?
"doTransform()" method is implemented, so don't care about it.
I just need to separate '+7913152', '8912345' and '916952', pass it to doTransform() and get an new List:
"'8913152','2020-05-25 00:00:25'"
"'8912345','2020-05-25 00:01:49'"
"'8916952','2020-05-25 00:01:55'"
Do it as follows:
List<String> result = list.stream()
.map(s -> {
String[] parts = s.split(",");
String st = doTransform(String.valueOf(Integer.parseInt(parts[0].replace("'", ""))));
return ("'" + st + "'") + "," + parts[1];
}).collect(Collectors.toList());
Demo:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("'+7913152','2020-05-25 00:00:25'");
list.add("'8912345','2020-05-25 00:01:49'");
list.add("'916952','2020-05-25 00:01:55'");
List<String> result = list.stream()
.map(s -> {
String[] parts = s.split(",");
String st = doTransform(String.valueOf(Integer.parseInt(parts[0].replace("'", ""))));
return ("'" + st + "'") + "," + parts[1];
}).collect(Collectors.toList());
// Display
result.forEach(System.out::println);
}
static String doTransform(String phone) {
return "x" + phone;
}
}
Output:
'x7913152','2020-05-25 00:00:25'
'x8912345','2020-05-25 00:01:49'
'x916952','2020-05-25 00:01:55'
I am assuming that length of the phone no in string as e.g "'+7913152','2020-05-25 00:00:25'" will be 7 if it's more than that you are going the remove the rest numbers from the head and replace with 8 to make it valid number else if number length is less than 7 then you will simply append the 8, Note: you can handle if the length of the number is like 3 or 4 etc.
public class PlayWithStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("'+7913152','2020-05-25 00:00:25'");
list.add("'8912345','2020-05-25 00:01:49'");
list.add("'916952','2020-05-25 00:01:55'");
List<String> collect = list.stream()
.map(PlayWithStream::doTransform)
.collect(Collectors.toList());
System.out.println(collect);
}
public static String doTransform(String phone1) {
String []a=phone1.split(",");
String phone=a[0];
boolean flag2 = phone.substring(0, 1).matches("[8]");
String finaloutput="";
if(!flag2) {
int len=phone.length();
if(len>7) {
String sub=phone.substring(0,phone.length()-6);
String newStr=phone.replace(sub, "");
finaloutput="8".concat(newStr);
}else {
finaloutput="8".concat(phone);
}
}
return finaloutput+","+a[1];
}
}
We can split elements using , in only two parts (initial which need to be change and remaining String) using limit on split String split.
List<String> list = list.stream()
.map(input -> input.split(",", 2))
.map(data -> String.join(",", doTransaform(data[0]), data[1]))
.collect(Collectors.toList());
It can be helpful when your string has multiple commas (,) separated parts and will surely improve efficiency too.
The below code works,
List<String> collect = list.stream()
.map(a -> a.replace(String.valueOf(a.charAt(1)), "2"))
.collect(Collectors.toList());

Java 8 Sort HashMap where map key is an object of <String, Integer>

I have a simple Customer class like so
public class Customer {
public int age;
public int discount;
public String name;
public Customer(String name) {
this.name = name;
}
public Customer(String name, int age) {
this.name = name;
this.age = age;
}
public Customer(String name, int age, int discount) {
this.name = name;
this.age = age;
this.discount = discount;
}
#Override
public String toString() {
return "Customer [age=" + age + ", discount=" + discount + ", name=" + name + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Integer getDiscount() {
return discount;
}
public void setDiscount(int discount) {
this.discount = discount;
}
}
I populate a list of these objects using this
List<Customer> customerList = new ArrayList<>(Arrays.asList(
new Customer("John", 2, 15),
new Customer("John", 4, 15),
new Customer("John", 6, 25),
new Customer("Joe", 3, 15),
new Customer("Joe", 3, 15),
new Customer("Joe", 3, 15),
new Customer("Goerge", 6, 25),
new Customer("Goerge", 6, 25),
new Customer("Mary", 7, 25),
new Customer("Jane", 1, 15),
new Customer("Jane", 2, 15),
new Customer("Jane", 8, 25),
new Customer("Jane", 8, 25)
));
Now I want to group and count the names and discounts, using a collector like this
Map<Object, Long> collected = customerList
.stream()
.collect(Collectors.groupingBy(x -> Arrays.asList(x.name, x.discount), Collectors.counting()));
I can review my output using this
collected.entrySet().forEach(c -> {
System.out.println(c);
});
Which outputs the following
[Jane, 15]=2
[Joe, 15]=3
[John, 15]=2
[Mary, 25]=1
[John, 25]=1
[Jane, 25]=2
[Goerge, 25]=2
The question is how do I sort the Map by name and discount so it looks like this
[Goerge, 25]=2
[Jane, 15]=2
[Jane, 25]=2
[Joe, 15]=3
[John, 15]=2
[John, 25]=1
[Mary, 25]=1
I keep bumping up against the Object type that is returned by the collector?
Can I cast the collector so that it returns a class, maybe something like
private class DiscountCounts
{
public String name;
public Integer discount;
}
Is it possible to convert the Map<**Object**, Long>() to something like Map<DiscountCounts, Long>(), would this allow access to the fields of the Map key using lambda or Comparator constructs?
I tried something like this, iterate over the map and manually convert to the Map I want but I can't get to the original collection's keys?
Map<DiscountCounts, Long> collected2 = new HashMap<>();
collected.entrySet().forEach(o -> {
DiscountCounts key1 = (DiscountCounts)o.getKey(); //--> Fails here
collected2.put((DiscountCounts)o.getKey(), o.getValue());
});
One way you can do it without using DiscountCounts class is, first sort the list and then perform the groping by operation, and use LinkedHashMap to save the sorted order
Map<List<Object>, Long> map = customerList.stream()
.sorted(Comparator.comparing(Customer::getName).thenComparing(Customer::getDiscount))
.collect(Collectors.groupingBy(x -> Arrays.asList(x.name, x.discount),LinkedHashMap::new, Collectors.counting()));
The another way using DiscountCounts class is, by override the equals and hashcode of DiscountCounts class and do a groupingBy creating DiscountCounts object for every Customer object as key in Map and use TreeMap with Comparator to sort the result
Map<DiscountCounts, Long> result = customerList.stream().collect(Collectors.groupingBy(
c -> new DiscountCounts(c.getName(), c.getDiscount()),
() -> new TreeMap<DiscountCounts, Long>(
Comparator.comparing(DiscountCounts::getName).thenComparing(DiscountCounts::getDiscount)),
Collectors.counting()));
#Andreas suggest in the comment enlighten me another way of doing it, and i feel this is one of the best approach you can implement Comparable on DiscountCounts and provide the sorting logic so that you don't need to provide Comparator to TreeMap
#Override
public int compareTo(DiscountCounts cust) {
int last = this.getName().compareTo(cust.getName());
return last == 0 ? this.getDiscount().compareTo(cust.getDiscount()) : last;
}
Map<DiscountCounts, Long> result1 = customerList.stream().collect(Collectors.groupingBy(
c -> new DiscountCounts(c.getName(), c.getDiscount()), TreeMap::new, Collectors.counting()));
With proper equals and hashcode implementation for DiscountCounts, you might be looking for something over the lines of :
Map<DiscountCounts, Long> collectSortedEntries = customerList
.stream()
.collect(Collectors.groupingBy(x -> new DiscountCounts(x.name, x.discount),
Collectors.counting()))
.entrySet()
.stream()
.sorted(Comparator.comparing((Map.Entry<DiscountCounts, Long> e) -> e.getKey().getName())
.thenComparing(e -> e.getKey().getDiscount()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));

Apply a function to a nested list grouped by stream

There is a list of objects for the following class:
class A {
String firstName;
String lastName;
//.....
}
There is also a method that takes in three parameters:
List<B> someMethod(String firstName, String lastName, List<A> l);
I want to group this list based on firstName and lastName and then apply this method to the items in the list of items that have firstName and lastName
I tried the following:
Map<String, Map<String, List<B>>> groupedItems = l.stream()
.collect(groupingBy(A::getFirstName, groupingBy(A::getLastName)));
List<B> result = groupedItems.keySet().stream()
.map(firstName -> groupedItems.get(firstName).keySet().stream()
.map(lastName -> someMethod(firstName, lastName, groupedItems.get(firstName).get(lastName))
.collect(Collectors.toList()))
.flatMap(Collection::stream)
.collect(Collectors::toList);
Is there a way to do this in one shot instead of the way it is now?
You can do it using collectingAndThen, but this, in my opinion, is far less readable.
List<B> result = l.stream().collect(Collectors.collectingAndThen(
groupingBy(A::getFirstName, groupingBy(A::getLastName)),
groupedItems -> groupedItems.keySet().stream()
.flatMap(firstName ->
groupedItems.get(firstName)
.keySet()
.stream()
.map(lastName ->
someMethod(
firstName,
lastName,
groupedItems.get(firstName).get(lastName)
)
)
)
.flatMap(Collection::stream)
.collect(toList())));
What about just iterate over gien List<A> and cache calculated results of Lit<B> for every unique fileName + lastName. It takes O(n) time and O(n) space.
public class Foo {
private final Map<String, List<B>> map = new HashMap<>();
public List<B> someMethod(String firstName, String lastName, List<A> as) {
return map.compute(firstName + '|' + lastName, (key, bs) -> {
if (bs == null) {
// create List<B> based on given List<A>
bs = Collections.emptyList();
}
return bs;
});
}
}

Java collection - group by multiple attributes

I have a Java class:
class Person {
String firstName;
String lastName;
int income;
public Person(String firstName, String lastName, int income)
{
this.firstName = firstName;
this.lastName = lastName;
this.income = income;
}
I have a Collection<Person>, with 4 x Person objects:
Collection<Person> persons = new ArrayList<>();
persons.add(new Person("John", "Smith", 5));
persons.add(new Person("Mary", "Miller", 2));
persons.add(new Person("John", "Smith", 4));
persons.add(new Person("John", "Wilson", 4));
I want to make a new Collection instance, but from the elements with the same "firstName" and "lastName", make 1 element, and the result "income" will be the sum of the "incomes" of each element. So, for this particular case, the resulting collection will have 3 elements, and "John Smith" will have the "income" = 9.
In SQL, the equivalent query is:
SELECT FIRSTNAME, LASTNAME, SUM(INCOME) FROM PERSON GROUP BY FIRSTNAME, LASTNAME
I found only answers which contain Map as result, and "key" contains the column(s) used for grouping by. I want to obtain directly a similar type of collection from the initial (ArrayList<Person>), and not a Map, because if in my collection I have millions of elements, it will decrease the performance of the code. I know it was easier if I worked on SQL side, but in this case I must work on Java side.
I don't know if that is the most beautiful solution, but you can try to groupBy firstName and lastName with a delimiter between them, let's say .. After you collect your data into Map<String, Integer> that contains your firstName.lastName, you create new list of Person from it.
List<Person> collect = persons.stream()
.collect(Collectors.groupingBy(person -> person.getFirstName() + "." + person.getLastName(),
Collectors.summingInt(Person::getIncome)))
.entrySet().stream().map(entry -> new Person(entry.getKey().split(".")[0],
entry.getKey().split(".")[1],
entry.getValue()))
.collect(Collectors.toList());
I think JoSQL is your way to go here, it allow you to run SQL queries over java objects:
JoSQL (SQL for Java Objects) provides the ability for a developer to apply a SQL statement to a collection of Java Objects. JoSQL provides the ability to search, order and group ANY Java objects and should be applied when you want to perform SQL-like queries on a collection of Java Objects.
And this is how to use it in your case:
Query q=new Query();
q.parse("SELECT firstname, lastname, SUM(income) FROM package.Person GROUP BY firstname, lastname");
List<?> results=q.execute(names).getResults();
You can also follow this JoSQL tutorial for further reading.
I found the answer below:
List<Person> collect = persons.stream()
.collect(Collectors.groupingBy(person -> person.getFirstName() + "." + person.getLastName(),
Collectors.summingInt(Person::getIncome)))
.entrySet().stream().map(entry -> new Person(entry.getKey().split(".")[0],
entry.getKey().split(".")[1],
entry.getValue()))
.collect(Collectors.toList());
Do not do that way! It uses memory a lot. Use Wrapper (PersonComparator) over the fields you need to group by.
public class Main {
public static void main(String[] args) {
Collection<Person> persons = new ArrayList<>();
persons.add(new Person("John", "Smith", 5));
persons.add(new Person("Mary", "Miller", 2));
persons.add(new Person("John", "Smith", 4));
persons.add(new Person("John", "Wilson", 4));
Map<Person, Integer> groupedByIncomes = persons.stream()
.collect(
Collectors.groupingBy(
Person::getPersonComparator,
Collectors.summingInt(Person::getIncome)
)
)
.entrySet()
.stream()
.collect(Collectors.toMap(
e -> e.getKey().person,
Map.Entry::getValue
));
System.out.println(groupedByIncomes);
}
static class Person {
String firstName;
String lastName;
int income;
public Person(String firstName, String lastName, int income) {
this.firstName = firstName;
this.lastName = lastName;
this.income = income;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getIncome() {
return income;
}
PersonComparator getPersonComparator() {
return new PersonComparator(this);
}
static class PersonComparator {
Person person;
PersonComparator(Person person) {
this.person = person;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonComparator that = (PersonComparator) o;
if (!person.getFirstName().equals(that.person.getFirstName())) return false;
return person.getLastName().equals(that.person.getLastName());
}
#Override
public int hashCode() {
int result = person.getFirstName().hashCode();
result = 31 * result + person.getLastName().hashCode();
return result;
}
}
}
}
If you need framework solution f.e. when you need some abstraction over the data types you have (SQL, Mongo or Collections) I suggest you to use QueryDSL: http://www.querydsl.com/
You can use Java 8 streams' Collector's groupingBy:
Map<String, Integer> sum = items.stream().collect(
Collectors.groupingBy(p -> p.getFirstName()+p.getSecondName(), Collectors.summingInt(Person::getIncome)));

Categories