Java 8 Map a List to another list and count - java

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.

Related

Java streams: return results to custom object

I have a piece of code that has list of objects as follows.
List<PivotMapEgModel> pivotMapList = List.of(new PivotMapEgModel(1L, "1"), new PivotMapEgModel(1L, "2"), new PivotMapEgModel(1L, "3"), new PivotMapEgModel(2L, "5"));
It is guaranteed that there will always be a maximum of 3 codes per value.
I have a class that looks like this:
#Data
#AllArgsConstructor
class ResultSet {
long value;
String code_1;
String code_2;
String code_3;
}
I am currently doing the stream operation in this way:
pivotMapList.stream().collect(Collectors.groupingBy(PivotMapEgModel::getValue, Collectors.mapping(PivotMapEgModel::getCode, Collectors.toList())))
This is producing the output in the following way: {1=[1, 2, 3], 2=[5]}
I need to perform stream operations on the pivotMapList to get the output to show in List<ResultSet> as follows:
[{value=1, code_1=1, code_2=2, code_3=3},
{value=2, code_1=1, code_2=null, code_3=null}]
I am not sure how I can get List<ResultSet> from stream operations
Any help to achieve the desired output would be greatly appreciated! Thank you
You have already mapped value to its codes. You can just continue by streaming the entry set of the resulting map and map entries to ResultSet.
List<ResultSet> result = pivotMapList.stream()
.collect(Collectors.groupingBy(PivotMapEgModel::getValue, Collectors.mapping(PivotMapEgModel::getCode, Collectors.toList())))
.entrySet()
.stream()
.map(entry -> new ResultSet(entry.getKey(), getCode(entry.getValue(), 0), getCode(entry.getValue(), 1), getCode(entry.getValue(), 2)))
.collect(Collectors.toList());
getCode() is simple method taking care not to get exception when retrieving values from the list.
private static String getCode(List<String> codes, int index) {
if (index >= codes.size()) {
return null;
}
return codes.get(index);
}
Here is one way.
class ResultSet {
long value;
String code_1;
String code_2;
String code_3;
public ResultSet(long value, String code_1,String code_2, String code_3) {
this.value = value;
this.code_1 = code_1;
this.code_2 = code_2;
this.code_3 = code_3;
}
public String toString() {
return "{%d, %s, %s, %s}".formatted(value, code_1, code_2, code_3);
}
}
Use your existing map to build the list. Stream the entrySet of the map and use map to instantiate a ResultSet instance. The forloop will fill the list with nulls if it isn't fully populated.
List<ResultSet> resultSet = map.entrySet().stream()
.<ResultSet>map(ent-> {
List<String> lst = ent.getValue();
for( int i = lst.size(); i < 3; i++) {
lst.add(null);
}
return new ResultSet(ent.getKey(), lst.get(0),
lst.get(1), lst.get(2));
}).toList();
resultSet.forEach(System.out::println);
prints
{1, 1, 2, 3}
{2, 5, null, null}
Note that you could simply stream the existing entrySet from the original map to combine the process, returning the desired List<ResultSet>

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()))
);

Getting the Set with the most elements nested in a HashMap using Java Streams

So here is the situation:
I need to register people's vote for certain dates. In short, a date is proposed and people vote for the date they want.
The data structure is the following:
private HashMap<LocalDateTime, Set<Vote>> votes;
A vote is:
public class Vote {
private String name;
private VoteType vote;
public Vote(String name, VoteType vote) {
super();
this.name = name;
this.vote = vote;
}
}
Where VoteType is just an enum:
public enum VoteType {YES, NO, MAYBE}
Now I already made a stream that returns the amount of votes for the availability (VoteType):
public Map<LocalDateTime, Integer> voteCount(VoteType targetVote) {
return this.votes.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new Integer(
e.getValue().stream().filter(v -> v.getVote() == targetVote).collect(Collectors.toList()).size())));
}
So my question is:
How can I get, using Java Streams, the date that got the most 'YES'.
/* Returns the date that got the most 'YES' votes */
public LocalDateTime winningDate() {
// TODO
}
Thank you for the help!
So my question is: How can I get, using Java Streams, the date that
got the most 'YES'.
This is going to be a lengthy one...
we need to get to a position where we have a Stream<LocalDateTime> so we can later group by date applying a counting downstream collector to get the number of votes on that specific date and we can accomplish this structure via flatMap.
we need to retain only the objects where the vote type is YES
we group the results by the date and have the values as the number of YES votes on that specific date.
we stream over the entrySet and find the max date by vote
Code:
/* Returns the date that got the most 'YES' votes */
public Optional<LocalDateTime> getWinningDate() {
return votes.entrySet() // Set<Entry<LocaleDateTime, Set<Vote>>
.stream() // Stream<Entry<LocaleDateTime, Set<Vote>>
.flatMap(e -> e.getValue().stream().filter(a -> a.getVote() == VoteType.YES)
.map(x -> e.getKey())) // Stream<LocalDateTime>
.collect(groupingBy(Function.identity(), counting())) // Map<LocaleDateTime, Long>
.entrySet() // Set<Entry<LocaleDateTime, Long>>
.stream() // Stream<Entry<LocaleDateTime, Long>>
.max(Comparator.comparingLong(Map.Entry::getValue)) // Optional<Entry<LocaleDateTime, Long>>
.map(Map.Entry::getKey); // Optional<LocalDateTime>
}
note that I've changed the method return type to
Optional<LocaleDateTime>, I could have returned
.map(Map.Entry::getKey).orElse(null) thus you've be able to maintain your current method return type of LocalDateTime but that just feels bad and so
I've decided to defer the decision upon what to do in the "no value
case" to the client.
I've changed the method name to getWinningDate to enhance readability.
As for dealing with Optional<T>, in your case, if you want to have a null value in the case of getWinningDate returning an empty Optional, you can unwrap it safely as:
LocalDateTime winningDate = getWinningDate().orElse(null);
or if you want to provide a default date:
LocalDateTime winningDate = getWinningDate().orElse(defaultDate);
or if you're sure there will always be a result then simply call get().
LocalDateTime winningDate = getWinningDate().get();
etc..
You can do it this way:
private LocalDateTime winningDate(Map<LocalDateTime, Integer> mapGroup) {
Integer max = mapGroup
.values().stream()
.max(Comparator.naturalOrder())
.get();
return mapGroup
.entrySet()
.stream()
.filter(e -> e.getValue().equals(max))
.map(Map.Entry::getKey)
.findFirst().orElse(null);
}
This answer shows a way to do it without your voteCount method but just in case you may want to write some logic in your winningDate method that would integrate with your already made voteCount method.
in which case we can do:
/* Returns the date that got the most 'YES' votes */
public Optional<LocalDateTime> getWinningDate() {
return voteCount(VoteType.YES).entrySet() // call voteCount and stream over the entries
.stream()
.max(Comparator.comparingLong(Map.Entry::getValue))
.map(Map.Entry::getKey);
}
first we invoke the voteCount(VoteType.YES) method to get a mapping of dates and the number of YES votes on the date.
second we find the max LocalDateTime by the vote count
note that I've changed the method return type to
Optional<LocaleDateTime>, I could have returned
.map(Map.Entry::getKey).orElse(null) thus you'll be able to maintain
your current method return type of LocalDateTime but that just feels
bad and so I've decided to defer the decision upon what to do in the
"no value case" to the client.
I've changed the method name to getWinningDate to enhance
readability.
Further, the voteCount method can be improved to:
public Map<LocalDateTime, Long> voteCount(VoteType targetVote) {
return this.votes.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue().stream().filter(v -> v.getVote() == targetVote).count()));
}
this avoids the overhead of constructing a list of all the elements that pass the filter simply to retrieve the count via size(), instead just filter and invoke count.
Use your first method which counts up the YES votes, returns a map of the yes counts which is passed into the winning date method:
/* Returns the date that got the most 'YES' votes */
public LocalDateTime winningDate(Map<LocalDateTime, Integer> yesVotes) {
return yesVotes.entrySet().stream().max(Map.Entry.comparingByValue()).get().getKey();
}
I can't help but thinking this was the intention here, but what do I know.
You asked how to do it with streams, here's another way:
class Max { long value = Long.MIN_VALUE; LocalDateTime date; }
Max max = new Max();
votes.forEach((d, vs) -> {
long count = vs.stream().filter(v -> VoteType.YES == v.getVote()).count();
if (count > max.value) {
max.value = count;
max.date = d;
}
});
LocalDateTime maxDate = max.date;
And to get the set of votes:
Set<Vote> maxVotesForYes = votes.get(maxDate);
This solution iterates the map entries and counts YES votes for each date. If this count is greater than the current max count, the max count (along with its corresponding date) is changed.
In order to be able to modify the max count and its corresponding date, we need a local class Max that keeps track of these values (otherwise, we wouldn't be able to change variables from within a lambda).
The question asked about how to solve this "using Java Streams". The following is using streams. And a for-loop.
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class VoteCountTest
{
public static void main(String[] args)
{
Map<LocalDateTime, Set<Vote>> votes =
new LinkedHashMap<LocalDateTime, Set<Vote>>();
Set<Vote> yes0 = votesWith(VoteType.NO, VoteType.NO);
Set<Vote> yes1 = votesWith(VoteType.YES, VoteType.NO);
Set<Vote> yes2 = votesWith(VoteType.YES, VoteType.YES);
votes.put(LocalDateTime.of(2000, 1, 1, 1, 1), yes1);
votes.put(LocalDateTime.of(2000, 1, 2, 1, 1), yes0);
votes.put(LocalDateTime.of(2000, 1, 3, 1, 1), yes2);
votes.put(LocalDateTime.of(2000, 1, 4, 1, 1), yes1);
System.out.println(getWinningDateA(votes));
System.out.println(getWinningDateB(votes));
}
public static Optional<LocalDateTime> getWinningDateA(
Map<LocalDateTime, Set<Vote>> votes)
{
LocalDateTime bestDate = null;
long maxCount = -1;
Predicate<Vote> votedYes = v -> v.getVote() == VoteType.YES;
for (Entry<LocalDateTime, Set<Vote>> entry : votes.entrySet())
{
long count = entry.getValue().stream().filter(votedYes).count();
if (count > maxCount)
{
maxCount = count;
bestDate = entry.getKey();
}
}
return Optional.ofNullable(bestDate);
}
// As of https://stackoverflow.com/a/53771478/3182664
public static Optional<LocalDateTime> getWinningDateB(Map<LocalDateTime, Set<Vote>> votes)
{
return votes.entrySet() // Set<Entry<LocaleDateTime, Set<Vote>>
.stream() // Stream<Entry<LocaleDateTime, Set<Vote>>
.flatMap(e -> e.getValue().stream().filter(a -> a.getVote() == VoteType.YES)
.map(x -> e.getKey())) // Stream<LocalDateTime>
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) // Map<LocaleDateTime, Long>
.entrySet() // Set<Entry<LocaleDateTime, Long>>
.stream() // Stream<Entry<LocaleDateTime, Long>>
.max(Comparator.comparingLong(Map.Entry::getValue)) // Optional<Entry<LocaleDateTime, Long>>
.map(Map.Entry::getKey); // Optional<LocalDateTime>
}
//=========================================================================
enum VoteType {YES, NO, MAYBE}
static class Vote {
private String name;
private VoteType vote;
public Vote(String name, VoteType vote) {
super();
this.name = name;
this.vote = vote;
}
public VoteType getVote()
{
return vote;
}
}
private static Set<Vote> votesWith(VoteType... voteTypes)
{
Set<Vote> votes = new LinkedHashSet<Vote>();
for (int i = 0; i < voteTypes.length; i++)
{
votes.add(new Vote("v" + i, voteTypes[i]));
}
return votes;
}
}
Compare this to the "pure-stream" solution, and think about which code you'd rather like to read, understand and maintain in the future. Then choose wisely.
(I know that this may, strictly speaking, not be the desired answer to the question. But some people seem to intentionally over-use streams and derive some sort of geeky pride from that. I also enjoy that as a challenge occasionally. But imagining that I might be the one who has to maintain these abominations of functional programming in the future makes me shudder...)

Sorting list of objects by date property

I have an object that contains two LocalDate properties:
public class SomeObject {
private LocalDate startDate;
private LocalDate endDate;
}
Constructor and stuff ommitted for brevity. I want to sort a list of these objects by their startdate and then assign the startdate of the next object to the previous object's enddate. To clarify, I start with a list of these objects:
SomeObject object1 = new SomeObject(LocalDate.parse("2015-01-01"), null);
SomeObject object2 = new SomeObject(LocalDate.parse("2014-01-01"), null);
SomeObject object3 = new SomeObject(LocalDate.parse("2016-01-01"), null);
List<SomeObject> list = Arrays.asList(object1, object2, object3);
And after sorting it should return this:
for (SomeObject object : list) {
System.out.println(object.startDate.toString() + " " + object.endDate.toString() );
}
2014-01-01 2015-01-01
2015-01-01 2016-01-01
2016-01-01 null
Each list will only contain 3 or 4 of these objects at most, but the code might have to process tens of thousands of these lists, so I'm looking for an efficient way to do this.
You can use Collections.sort with a Comparator. In Java 8 with Lambdas it looks like this:
Collections.sort(list, (x, y) -> x.startDate.compareTo(y.startDate));
for (int i = 0; i < (list.size() - 1); i++) {
list.get(i).endDate = list.get(i + 1).startDate;
}
As an enhancement to the accepted answer:
Collections.sort(list, Comparator.comparing(SomeObject::getStartDate));
As you mentioned that you didn't really care whether it is startDate or endDate and just order all of them, maybe the following will help you:
List<LocalDate> dates = list.stream()
.flatMap(s -> Stream.of(s.startDate, s.endDate))
.filter(Objects::nonNull) // maybe... if nulls are required too, then skip that part here... (but also check the sorting variant then); note that I use null now if the last date is reached (check the printing part for that)
.distinct()
.sorted() // natural order
// alternatively: natural order + nulls last
// .sorted(Comparator.nullsLast(Comparator.comparing(Function.identity())))
.collect(Collectors.toList());
// printing part:
IntStream.range(0, dates.size())
.mapToObj(i -> {
String from = Objects.toString(dates.get(i));
String upto = Objects.toString(i < dates.size() - 1 ? dates.get(i + 1) : null); // exchange null with the end date you are expecting
return from + " - " + upto;
})
.forEach(System.out::println);
EDIT: There was that endDate set on one of your samples before... as that isn't the case anymore, here an update how you can set the right date ranges. It's basically similar to what also Ralf Renz has used in his answer:
list.sort(Comparator.comparing(SomeObject::getStartDate));
IntStream.range(0, list.size() - 1)
.forEach(i -> list.get(i).endDate = list.get(i + 1).startDate);
// or if you care about performance, just do the same as Ralf did:
for (int i = 0; i < (list.size() - 1); i++) {
list.get(i).endDate = list.get(i + 1).startDate;
}
Make use of the fact that LocalDate already implements Comparable and make your SomeObject do as well. Additionally, give it a proper toString() method, which handles null values in order to represent your object as a String:
public class SomeObject implements Comparable<SomeObject> {
private LocalDate startDate;
private LocalDate endDate;
public SomeObject(LocalDate startDate, LocalDate endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
#Override
public int compareTo(SomeObject anotherObject) {
return this.startDate.compareTo(anotherObject.startDate);
}
#Override
public String toString() {
String start = startDate == null ? "null" : startDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
String end = endDate == null ? "null" : endDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
StringBuilder sb = new StringBuilder();
sb.append(start).append(" ").append(end);
return sb.toString();
}
}
By doing so, you can easily just call Collections.sort(list); and have your data sorted by startDate:
public class SomeObjectSorting {
public static void main(String[] args) {
SomeObject object1 = new SomeObject(LocalDate.parse("2015-01-01"), null);
SomeObject object2 = new SomeObject(LocalDate.parse("2014-01-01"), LocalDate.parse("2017-01-01"));
SomeObject object3 = new SomeObject(LocalDate.parse("2016-01-01"), null);
List<SomeObject> list = Arrays.asList(object1, object2, object3);
System.out.println("———— BEFORE SORTING ————");
list.forEach(object -> {
System.out.println(object.toString());
});
Collections.sort(list);
System.out.println("———— AFTER SORTING ————");
list.forEach(object -> {
System.out.println(object.toString());
});
}
}

Merge 2 lists in a Functional Reactive way

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+"]";
}
}

Categories

Resources