Merge 2 lists in a Functional Reactive way - java

Let's imagine the following object :
class People {
public int id;
public String name;
public Date dateOfDeath;
}
I have 2 lists of people.
In the first one, a People object has its ID and NAME properly set. In the second one, a People object has its ID and DATEOFDEATH properly set.
I need to combine the 2 lists in order to have a single list with a full People object (name and date of death).
In a full procedural way, this could be done with a double for loop like this :
for (People fullPeople : firstList) {
for (People peopleWithDateOfDeath : secondList) {
if (peopleWithDateOfDeath.id == fullPeople.id) {
fullPeople.dateOfDeath = peopleWithDateOfDeath.dateOfDeath;
break;
}
}
}
secondList = null;
// first list is good :)
How can I implement this in a functional way? I am using Rx-Java but any example with Java 8 Streams is easily convertible.

You can avoid O(n2) complexity by building a map of id to dateOfDeath:
Map<Integer, Date> deaths = secondList.stream()
.collect(toMap(p -> p.id, p -> p.dateOfDeath));
fullPeople.stream()
.filter(p -> deaths.containsKey(p.id))
.forEach(p -> p.dateOfDeath = deaths.get(p.id));
Or, if you want to avoid mutating existing people:
List<People> mergedPeople = fullPeople.stream()
.map(p -> deaths.containsKey(p.id)
? new People(p.id, p.name, deaths.get(p.id))
: p
).collect(toList());

You could do it like this:
List<People> persons =
names.stream()
.map(p -> new People(p.id, p.name, dates.stream()
.filter(pd -> pd.id == p.id)
.map(pd -> pd.dateOfDeath)
.findFirst()
.orElse(null))
)
.collect(Collectors.toList());
where names is the list of persons having the names and dates is the list of persons having the date of death. This assumes that the People class has a 3 argument constructor taking the id, name and date of death.
For all person with names, the person having the same id is looked up in the other list with filter and we map the result to the dateOfDeath. If a match is found, the date is returned, otherwise, orElse is invoked and null is returned.
Note that this will not merge any person that is present in the dates list but not in the names list.
Sample code:
List<People> names = new ArrayList<>();
List<People> dates = new ArrayList<>();
names.add(new People(1, "Name 1", null));
names.add(new People(2, "Name 2", null));
dates.add(new People(1, null, new Date()));
dates.add(new People(3, null, new Date()));
List<People> peoples = codeFromAbove();
System.out.println(peoples);
// prints
// [[id=1, name=Name 1, date=Sun Oct 18 19:48:58 CEST 2015],
// [id=2, name=Name 2, date=null]]
with:
class People {
public int id;
public String name;
public Date dateOfDeath;
public People(int id, String name, Date dateOfDeath) {
this.id = id;
this.name = name;
this.dateOfDeath = dateOfDeath;
}
#Override
public String toString() {
return "[id="+id+", name="+name+", date="+dateOfDeath+"]";
}
}

Related

Using Java Streams API to dynamically convert a list into a group map with enum key

Java 8 Streams here. I have the following classes:
public enum Category {
Thing,
Thang,
Fizz
}
#Data // using lombok to generate ctors/getters/setters/etc.
public class LineItem {
private Long id;
private String name;
private Category category;
private BigDecimal amount;
}
#Data
public class PieSlice {
private String label;
private BigDecimal value = BigDecimal.ZERO;
public void addAmount(BigDecimal amount) {
value = value.add(amount);
}
}
In my code I am given a List<LineItem> and I want to convert it to a Map<Category,PieSlice> using the Streams API, if at all possible.
Using the non-Stream way, the conversion would look like:
List<LineItem> lineItems = getSomehow();
Map<Category,PieSlice> sliceMap = new HashMap<>();
PieSlice thingSlice = new PieSlice();
PieSlice thangSlice = new PieSlice();
PieSlice fizzSlice = new PieSlice();
for (LineItem lineItem : lineItems) {
if (lineItem.getCategory().equals(Category.Thing)) {
thingSlice.addAmount(lineItem.getAmount());
} else if (lineItem.getCategory().equals(Category.Thang)) {
thangSlice.addAmount(lineItem.getAmount());
} else if (lineItem.getCategory().equals(Category.Fizz)) {
fizz.addAmount(lineItem.getAmount());
} else {
throw new RuntimeException("uncategorized line item");
}
}
sliceMap.put(Category.Thing, thingSlice);
sliceMap.put(Category.Thang, thangSlice);
sliceMap.put(Category.Fizz, fizzSlice);
The problem is that I need to edit the code every time I add a new Category. Is there a way to do this via the Streams API, regardless of what Category values exist?
Try this.
List<LineItem> lineItems = List.of(
new LineItem(1L, "", Category.Thing, BigDecimal.valueOf(100)),
new LineItem(2L, "", Category.Thang, BigDecimal.valueOf(200)),
new LineItem(3L, "", Category.Fizz, BigDecimal.valueOf(300)),
new LineItem(4L, "", Category.Thing, BigDecimal.valueOf(400))
);
Map<Category, PieSlice> sliceMap = lineItems.stream()
.collect(
groupingBy(LineItem::getCategory,
mapping(LineItem::getAmount,
collectingAndThen(
reducing(BigDecimal.ZERO, BigDecimal::add),
amount -> {
PieSlice pieSlice = new PieSlice();
pieSlice.addAmount(amount);
return pieSlice;
}))));
sliceMap.entrySet().stream()
.forEach(System.out::println);
output:
Fizz=PieSlice [label=null, value=300]
Thang=PieSlice [label=null, value=200]
Thing=PieSlice [label=null, value=500]
You can use the collect operation to achieve this
Map<Category, PieSlice> sliceMap = lineItems
.stream()
.collect(
Collectors.groupingBy(
LineItem::getCategory,
Collectors.reducing(
new PieSlice(),
item -> {
PieSlice slice = new PieSlice();
slice.addAmount(item.getAmount());
return slice;
},
(slice, anotherSlice) -> {
slice.addAmount(anotherSlice.getValue());
return slice;
}
)
)
);
What this piece of code does is a 2-step reduction. First, we take lineItems and group them by their category - reducing the initial list to a map, we achieve this by using Collectors.groupingBy. If we were to use this collector without the second argument, the result would be of type Map<Category, List<LineItem>>. Here is where the Collectors.reducing reducer comes to play - it takes the list of LineItems which are already grouped by their category and turns them into a singular PieSlice, where the original values are accumulated.
You can read more on reduction operations and the standard reducers provided by the JDK here.
The problem is that I need to edit the code every time I add a new Category. Is there a way to do this via the Streams API, regardless of what Category values exist?
You can obtain all declared enum-constants using either values() or EnumSet.allOf(Class<E>).
If you need the resulting map to contain the entry for every existing Category-member, you can provide a prepopulated map through the supplier of collect() operation.
Here's how it might be implemented:
Map<Category, PieSlice> sliceMap = lineItems.stream()
.collect(
() -> EnumSet.allOf(Category.class).stream()
.collect(Collectors.toMap(Function.identity(), c -> new PieSlice())),
(Map<Category, PieSlice> map, LineItem item) ->
map.get(item.getCategory()).addAmount(item.getAmount()),
(left, right) ->
right.forEach((category, slice) -> left.get(category).addAmount(slice.getValue()))
);

How to split List into list of lists at run time based on property values

How to split below list into list of lists based on name , salary and joining date without checking values. I want to do at runtime because I don't know values:
public class Employee {
private String name;
private long id;
BigDecimal sal;
private Date joinDate;
public Employee(String name, long id, BigDecimal sal,Date date) {
super();
this.name = name;
this.id = id;
this.sal = sal;
this.joinDate=date;
}
}
List<Employee> employeeList =new ArrayList<>();
SimpleDateFormat format =new SimpleDateFormat("MMddYYYY");
employeeList.add(new Employee("Mike",1000,BigDecimal.valueOf(200),format.parse("08122022")));
employeeList.add(new Employee("David",1001,BigDecimal.valueOf(100),format.parse("08112022")));
employeeList.add(new Employee("Mike",1002,BigDecimal.valueOf(200),format.parse("08122022")));
employeeList.add(new Employee("David",1003,BigDecimal.valueOf(100),format.parse("08112022")));
employeeList.add(new Employee("David",1003,BigDecimal.valueOf(200),format.parse("08112022")));
employeeList.add(new Employee("David",1003,BigDecimal.valueOf(200),format.parse("08122022")));
Output should be list of list employee objects , above list should return 3 list of employee objects
If I run below it will give me list contain Mike objects but at runtime I don’t know what data list contains so I can’t do really this , so basically # runtime above list should compare elements
And final list should be list of list employee objects
List result = employeeList.stream().filter(emp-> emp.getName().equals("Mike"))
.collect(Collectors.toList())
Based on your example, you could attempt something like the following:
List<List<Employee>> groupByName = new ArrayList<>();
employeeList.forEach(e -> {
// Automatically start a new list and add first employee if no lists is present
if(groupByName.isEmpty()) {
List<Employee> newList = new ArrayList<>();
newList.add(e);
groupByName.add(newList);
}
else {
// use a boolean to track whether the employee has been matched to a list
boolean match = false;
groupByName.forEach(l -> {
// compare each employee to the first employee in each list
if(l.get(0).getName().equals(e.getName()) {
// if the names match, add to the current list, and break out of loop
l.add(e);
match = true;
break;
}
});
// if no match has been found for the current name, create a new list and add the employee
if(!match) {
List<Employee> newList = new ArrayList<>();
newList.add(e);
groupByName.add(newList);
}
});
Using a similar process, you could filter the lists based on the other criteria as well.

How to get a list output from forEach loop in Java 8 Streams

I have two different lists of same objects but different properties and with a common identifier in those objects. I would like to iterate over the first list and get the corresponding object from the second (which has common properties) and then wrap those objects around and finally add that object to a list using Java Streams.
This is the example I have taken.
private class Person {
private String name;
private boolean isSenior;
private Person(String name, boolean isSenior) {
this.name = name;
this.isSenior = isSenior;
}
public String getName() {
return name;
}
public boolean isSenior() {
return isSenior;
}
#Override
public String toString() {
return name + ": " + isSenior;
}
}
private class PersonWrapper {
private Person jrPerson;
private Person srPerson;
private PersonWrapper(Person jrPerson, Person srPerson) {
this.jrPerson = jrPerson;
this.srPerson = srPerson;
}
public Person getJrPerson() {
return jrPerson;
}
public Person getSrPerson() {
return srPerson;
}
#Override
public String toString() {
return jrPerson.toString() + "-" + srPerson.toString();
}
}
Now, in the main class, I will create two list instances like this
List<Person> jrPersons = new ArrayList<>();
List<Person> srPersons = new ArrayList<>();
and add the objects in the following manner
jrList.add(new Person("John", false));
jrList.add(new Person("Paul", false));
jrList.add(new Person("Mike", false));
seniorList.add(new Person("John", true));
seniorList.add(new Person("Paul", true));
seniorList.add(new Person("Mike", true));
Now, I want to iterate over the jrList and find the corresponding Person object in the srList (same name). Then I would wrap these objects as PersonWrapper and that object to a list.
So far, this is what I have been doing
List<PersonWrapper> wrapperList = new ArrayList<>();
jrList.forEach(jr -> seniorList.stream().filter(sr -> jr.getName().equals(sr.getName())).map(sr -> new PersonWrapper(jr, sr)).collect(Collectors.toList()));
Now, I would like to know how the Collectors.toList() can be substituted by wrapperList or how the output from Collectors.toList() be added to wrapperList.
Please help me in achieving this.
Instead of using a forEach just use streams from the beginning:
List<PersonWrapper> wrapperList = jrList.stream()
.flatMap(jr -> seniorList.stream()
.filter(sr -> jr.getName().equals(sr.getName()))
.map(sr -> new PersonWrapper(jr, sr))
)
.collect(Collectors.toList());
By using flatMap you can flatten a stream of streams (Stream<Stream<PersonWrapper>>) into a single stream (Stream<PersonWrapper>)
If you can't instantiate wrapperList by yourself or really need to append to it. You can alter above snippet to following:
List<PersonWrapper> wrapperList = new ArrayList<>();
jrList.stream()
.flatMap(jr -> seniorList.stream()
.filter(sr -> jr.getName().equals(sr.getName()))
.map(sr -> new PersonWrapper(jr, sr))
)
.forEach(wrapperList::add);
While Lino's answer is certainly correct. I would argue that if a given person object in jrList can only ever have one corresponding match in seniorList maximum, in other words, if it's a 1-1 relationship then you can improve upon the solution given by Lino by finding the first match as follows:
List<PersonWrapper> resultSet = jrList.stream()
.map(p -> seniorList.stream()
.filter(sr -> p.getName().equals(sr.getName()))
.findFirst()
.map(q -> new PersonWrapper(p, q))
.get())
.collect(Collectors.toList());
or if there is no guarantee that each person in jrList will have a corresponding match in seniorList then change the above query to:
List<PersonWrapper> resultSet = jrList.stream()
.map(p -> seniorList.stream()
.filter(sr -> p.getName().equals(sr.getName()))
.findFirst()
.map(q -> new PersonWrapper(p, q))
.orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
The difference is that now instead of calling get() on the result of findFirst() we provide a default with orElse in case findFirst cannot find the corresponding value and then we filter the null values out in the subsequent intermediate operation as they are not needed.
Replace your looping logic with below code.
jrList.forEach(jr -> seniorList.stream().filter(sr -> jr.getName().equals(sr.getName()))
.map(sr -> wrapperList.add(new PersonWrapper(jr, sr))).collect(Collectors.toList()));

Java 8 Map a List to another list and count

I am looking to create a list of history values for an existing list so that I can save it in DB to be displayed later in a table
Class Data {
Date date;
int int1;
int int2;
}
class DataHistory {
Date date;
int sum_Int1_beforeOrEqualDate;
int sum_Int2_beforeOrEqualDate;
String someOtherValues;
}
For example I have several lines perDate with all values. What I would like to achieve is :
My input :
date, int1, int2
01/01/18, 2, 3
01/01/18, 0, 1
02/01/18, 0, 1
02/01/18, 3, 0
03/01/18, 1, 3
...
My output :
date, sum_Int1_beforeOrEqualDate, sum_Int2_beforeOrEqualDate
01/01/18, 2, 4
02/01/18, 3, 1
03/01/18, 1, 3
...
I have tried several things, mainly with Map, but has never been able to do it with List-->List.
What I have tried to do is :
Edit: My lastAttempt, which clearly shows I don't know what i am doing..
List<OutputList> outputList =
inputlist
.stream()
.map( e -> new DataHistory())
.collect(Collectors.groupingBy(int1));
I believe you're trying to simply sum the values grouping by date. So assuming you have parsed data as a List
List<Data> list = getDataAsList();
List<DataHistory> historyList = list.stream()
.collect(Collectors.groupingBy(data -> data.date)).entrySet().stream()
.map((entry) -> {
DataHistory history = new DataHistory();
history.date = entry.getKey();
List<Data> dataList = entry.getValue();
history.sum_Int1_beforeOrEqualDate = dataList.stream().mapToInt(data -> data.int1).sum();
history.sum_Int2_beforeOrEqualDate = dataList.stream().mapToInt(data -> data.int2).sum();
return history;
})
.collect(Collectors.toList());
Tell me if I got the logic correct.
What you could do is use Collections.reducing which works pretty good.
List<DataHistory> dataHistories =
list.stream()
.collect(Collectors.groupingBy(Data::getDate,
Collectors.reducing(DataHistory::new,
DataHistoryHelper::merge)))
.values();
This solution assumes you have a constructor in DataHistory taking a Data as parameter.
public DataHistory(Data o) {
this.date = o.getDate();
// and so on
}
And that you have a method (anywhere) that takes care of merging two DataHistory objects
public DataHistory merge(DataHistory o1, DataHistory o2) {
DataHistory merged = new DataHistory();
merged.setSum_Int1_beforeOrEqualDate(o1.getSum_Int1_beforeOrEqualDate + o2.getSum_Int1_beforeOrEqualDate);
// and so on
return merged;
}
You can accomplish the task at hand using the toMap collector:
Collection<DataHistory> resultSet =
myList.stream()
.collect(Collectors.toMap(Data::getDate,
e -> new DataHistory(e.getDate(), e.getInt1(), e.getInt2(), null),
DataHistory::merge)).values();
This assumes you have a constructor defined as follows in your DataHistory class:
public DataHistory(Date date, int sum_Int1_beforeOrEqualDate,
int sum_Int2_beforeOrEqualDate, String someOtherValues) {
this.date = date;
this.sum_Int1_beforeOrEqualDate = sum_Int1_beforeOrEqualDate;
this.sum_Int2_beforeOrEqualDate = sum_Int2_beforeOrEqualDate;
this.someOtherValues = someOtherValues;
}
and a merge function defined as such:
public DataHistory merge(DataHistory other){
this.sum_Int1_beforeOrEqualDate += other.getSum_Int1_beforeOrEqualDate();
this.sum_Int2_beforeOrEqualDate += other.getSum_Int2_beforeOrEqualDate();
return this;
}
in the DataHistory class.
Further, if you explicitly require a List<DataHistory> as opposed to a Collection<DataHistory> then you can do:
List<DataHistory> historyList = new ArrayList<>(resultSet);
Note that I am passing null to the DataHistory constructor for the fourth parameter simply because I don't know what data to pass, so I'll leave that for you to decide upon.

Merging objects from two list with unique id using Java 8

class Human {
Long humanId;
String name;
Long age;
}
class SuperHuman {
Long superHumanId;
Long humanId;
String name;
Long age;
}
I've two lists. List of humans and List of superHumans. I want to create a single list out of the two making sure that if a human is superhuman, it only appears once in the list using java 8. Is there a neat way to do it?
UPDATE: These are different classes i.e. neither extends the other. I want the final list to be of superhumans. If a human already is superhuman, we ignore that human object. If a human is not a superhuman, we convert the human object into the super human object. Ideally I would want to sort them by their age at the end so that I get a list of superhuman sorted by date in descending order.
Based on your updated question:
List<Human> humans = ... // init
List<SuperHuman> superHumans = ... // init
Set<Long> superHumanIds = superHumans.stream()
.map(SuperHuman::getHumanId)
.collect(toSet());
humans.stream()
.filter(human -> superHumanIds.contains(human.getHumanId()))
.map(this::convert)
.forEach(superHumans::add);
superHumans.sort(Comparator.comparing(SuperHuman::getAge));
Assuming this class has another method with the following signature:
private Superhuman convert(Human human) {
// mapping logic
}
You do have other suggestions about how your code should be re-factored to make it better, but in case you can't do that, there is a way - via a custom Collector that is not that complicated.
A custom collector also gives you the advantage of actually deciding which entry you want to keep - the one that is already collected or the one that is coming or latest in encounter order wins. It would require some code changes - but it's doable in case you might need it.
private static <T> Collector<Human, ?, List<Human>> noDupCollector(List<SuperHuman> superHumans) {
class Acc {
ArrayList<Long> superIds = superHumans.stream()
.map(SuperHuman::getHumanId)
.collect(Collectors.toCollection(ArrayList::new));
ArrayList<Long> seen = new ArrayList<>();
ArrayList<Human> noDup = new ArrayList<>();
void add(Human elem) {
if (superIds.contains(elem.getHumanId())) {
if (!seen.contains(elem.getHumanId())) {
noDup.add(elem);
seen.add(elem.getHumanId());
}
} else {
noDup.add(elem);
}
}
Acc merge(Acc right) {
noDup.addAll(right.noDup);
return this;
}
public List<Human> finisher() {
return noDup;
}
}
return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finisher);
}
Supposing you have these entries:
List<SuperHuman> superHumans = Arrays.asList(
new SuperHuman(1L, 1L, "Superman"));
//
List<Human> humans = Arrays.asList(
new Human(1L, "Bob"),
new Human(1L, "Tylor"),
new Human(2L, "John"));
Doing this:
List<Human> noDup = humans.stream()
.collect(noDupCollector(superHumans));
System.out.println(noDup); // [Bob, Tylor]
Try this.
List<Object> result = Stream.concat(
humans.stream()
.filter(h -> !superHumans.stream()
.anyMatch(s -> h.humanId == s.humanId)),
superHumans.stream())
.collect(Collectors.toList());
Suppose neither of the classes inherit the other one. I can imagine you have two lists:
Human alan = new Human(1, "Alan");
Human bertha = new Human(2, "Bertha");
Human carl = new Human(3, "Carl");
Human david = new Human(4, "David");
SuperHuman carlS = new SuperHuman(1, 3, "Carl");
SuperHuman eliseS = new SuperHuman(2, 0, "Elise");
List<Human> humans = new ArrayList<>(Arrays.asList(alan, bertha, carl, david));
List<SuperHuman> superHumans = new ArrayList<>(Arrays.asList(carlS, eliseS));
We see that Carl is listed as a human, and also as a superhuman. Two instances of the very same Carl exist.
List<Object> newList = humans.stream()
.filter((Human h) -> {
return !superHumans.stream()
.anyMatch(s -> s.getHumanId() == h.getHumanId());
})
.collect(Collectors.toList());
newList.addAll(superHumans);
This code tries to filter the list of humans, excluding all entries whose humanId exist in the list of superhumans. At last, all superhumans are added.
This design has several problems:
Intuitively, the classes are related, but your code says otherwise. The fact that you are trying to merge, suggests the types are related.
The classes have overlapping properties (humanId and name) which as well suggest that the classes are related.
The assumption that the classes are related, is definitely not unfounded.
As other commenters suggested, you should redesign the classes:
class Human {
long id; // The name 'humanId' is redundant, just 'id' is fine
String name;
}
class SuperHuman extends Human {
long superHumanId; // I'm not sure why you want this...
}
Human alan = new Human(1, "Alan");
Human bertha = new Human(2, "Bertha");
Human carl = new SuperHuman(3, "Carl");
Human david = new Human(4, "David");
Human elise = new SuperHuman(5, "Elise");
List<Human> people = Arrays.asList(alan, bertha, carl, david, elise);
Then you have one single instance for each person. Would you ever get all superhumans from the list, just use this:
List<Human> superHumans = people.stream()
.filter((Human t) -> t instanceof SuperHuman)
.collect(Collectors.toList());
superHumans.forEach(System.out::println); // Carl, Elise

Categories

Resources