I have POJO definition as follows:
class EmployeeDetails{
private String deptName;
private Double salary;
private Double bonus;
...
}
Currently, i have lambda expression for Group By 'deptName' as :
$set.stream().collect(Collectors.groupingBy(EmployeeDetails::getDeptName,
Collectors.summingLong(EmployeeDetails::getSalary));
Question Is it possible to Sum more than one column? I need to compute sum on both fields salary and bonus in one expression instead of multiple times?
SQL representation would be:
SELECT deptName,SUM(salary),SUM(bonus)
FROM TABLE_EMP
GROUP BY deptName;
You need to create an additional class that will hold your 2 summarised numbers (salary and bonus). And a custom collector.
Let's say you have
private static final class Summary {
private double salarySum;
private double bonusSum;
public Summary() {
this.salarySum = 0;
this.bonusSum = 0;
}
#Override
public String toString() {
return "Summary{" +
"salarySum=" + salarySum +
", bonusSum=" + bonusSum +
'}';
}
}
for holding sums. Then you need a collector like this:
private static class EmployeeDetailsSummaryCollector implements Collector<EmployeeDetails, Summary, Summary> {
#Override
public Supplier<Summary> supplier() {
return Summary::new;
}
#Override
public BiConsumer<Summary, EmployeeDetails> accumulator() {
return (summary, employeeDetails) -> {
summary.salarySum += employeeDetails.salary;
summary.bonusSum += employeeDetails.bonus;
};
}
#Override
public BinaryOperator<Summary> combiner() {
return (summary, summary1) -> {
summary.salarySum += summary1.salarySum;
summary.bonusSum += summary1.bonusSum;
return summary;
};
}
#Override
public Function<Summary, Summary> finisher() {
return Function.identity();
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
}
}
With these classes you can collect your results like
final List<EmployeeDetails> employees = asList(
new EmployeeDetails(/* deptName */"A", /* salary */ 100d, /* bonus */ 20d),
new EmployeeDetails("A", 150d, 10d),
new EmployeeDetails("B", 80d, 5d),
new EmployeeDetails("C", 100d, 20d)
);
final Collector<EmployeeDetails, Summary, Summary> collector = new EmployeeDetailsSummaryCollector();
final Map<String, Summary> map = employees.stream()
.collect(Collectors.groupingBy(o -> o.deptName, collector));
System.out.println("map = " + map);
Which prints this:
map = {A=[salary=250.0, bonus=30.0], B=[salary=80.0, bonus=5.0], C=[salary=100.0, bonus=20.0]}
I know you've got your answer, but here is my take(I was writing while the other was posted). There is already a Pair in java in the form of AbstractMap.SimpleEntry.
System.out.println(Stream.of(new EmployeeDetails("first", 50d, 7d), new EmployeeDetails("first", 50d, 7d),
new EmployeeDetails("second", 51d, 8d), new EmployeeDetails("second", 51d, 8d))
.collect(Collectors.toMap(EmployeeDetails::getDeptName,
ed -> new AbstractMap.SimpleEntry<>(ed.getSalary(), ed.getBonus()),
(left, right) -> {
double key = left.getKey() + right.getKey();
double value = left.getValue() + right.getValue();
return new AbstractMap.SimpleEntry<>(key, value);
}, HashMap::new)));
Grouping by is a terminal operation that yields a map. The map produced by the groupingBy in the code below is a Map<String, List<EmployeeDetails>>. I create a new stream using the Map entrySet method. I then create a new Map using Collectors.toMap. This approach uses method chaining to avoid creating another class and create more concise code.
details.stream()
.collect(Collectors.groupingBy(EmployeeDetails::getDeptName))
.entrySet()
.stream()
.collect(Collectors.toMap(x->x.getKey(), x->x.getValue()
.stream()
.mapToDouble(y -> y.getSalary() + y.getBonus())
.sum()));
Related
There is code
class Person {
private ZonedDateTime date ;
private int regionId;
private int centerId;
private int amount1;
private float percent1;
}
List<Person> entityList = new ArrayList<>();
I grouping by year of month like this:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(),Collectors.collectingAndThen(Collectors.toList(),
l -> {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
))).forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
Also i group by year, regionId, centerId in same manner:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getYear(),Collectors ......
But i got many duplicate code where part with
l -> {...}
repeated many times. How to instead of l -> {...} use a method reference?
IntelliJ can literally just do this for you. You don't even have to think about it.
Keyboard shortcut for hints (yellow) is AltEnter
Here's what I ended up with
public static void main(String[] args)
{
List<Person> listPerson = null;
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Scratch::apply
)))
.forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
}
private static List<String> apply(List<Person> l)
{
int sumAmount1 = l.stream().mapToInt(Person::getAmount1).sum();
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(Person::getPercent1));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
You can create a method reference like this:
private List<String> methodReference(List<Person> l) {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
I have created a methodReference in my Test class. You can replace it with your own class name. And now in your stream() you can refer to it like this:
entityList.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Test::methodReference // replace Test with your class name
))).forEach((k, v) -> System.out.println(k.getValue() + "-" + v.toString()));
Might be a little of topic, but I think the beauty of using Stream API is allowing you to build a data pipeline. I would strive to build something that looks like a pipeline, with steps I can customize with pluggable functions.
I think the code would be more readable by refactoring towards a pipeline, and I would try below with the help of a new data structure called Tuple2, epscially its map method. It's easy to build, you can also use one from libraries like vavr.
For reuse, one can consider a function like groupAndSummarize (the name suggests it does two things, so is a smell).
class Tuple2<T1, T2> {
private final T1 t1;
private final T2 t2;
public Tuple2(final T1 t1, final T2 t2) {
this.t1 = t1;
this.t2 = t2;
}
public <U1, U2> Tuple2<U1, U2> map(final Function<T1, U1> func1,
final Function<T2, U2> func2) {
return new Tuple2<>(func1.apply(t1), func2.apply(t2));
}
public T1 _1() { return t1; }
public T2 _2() { return t2; }
}
private <T, K, V> List<Tuple2<K, V>> groupAndSummarize(final List<T> list, final Function<T, K> groupFn, final Function<List<T>, V> summarizeFn) {
return list.stream()
.collect(Collectors.groupingBy(groupFn))
.entrySet()
.stream()
.map(this::toTuple)
.map(t -> t.map(
Function.identity(),
summarizeFn
))
.collect(Collectors.toList());
}
private <K, V> Tuple2<K, V> toTuple(final Map.Entry<K, V> entry) {
return new Tuple2<>(entry.getKey(), entry.getValue());
}
private List<String> summarize(final List<Person> l) {
// your logic
}
public void test() {
final List<Person> entityList = new ArrayList<>();
groupAndSummarize(entityList, i -> i.getDate().getMonth(), this::summarize)
.forEach(t -> System.out.println(t.t1.getValue() + "-" + t.t2.toString()));
}
Assuming I consume a Stream of entities from a source which I do not want to materialize, and I want to both transform the elements, and return some globally reduced value, what is the idiomatic way with java(8)?
This is essentially trying to perform both a reduce() and a collect().
Example:
class Person {
public String firstname,
public String lastname,
public int age;
}
class TeamSummary {
public List<String> fullnames, // firstname and lastname of all
public Person oldest
}
public TeamSummary getSummary(Stream<Person> personStream) {
final TeamSummary summary = new Summary();
summary.fullnames = personStream
.peek(p -> if (summary.getOldest() == null || summary.getOldest.age < p.age) {
summary.oldest = p;
})
.map(p -> p.firstname + ' ' + p.lastname)
.collect(toList());
return summary;
}
It looks ugly to interact with a variable outside the stream inside the peek method, but what good alternatives are there, it seems I need to combine collect() and reduce().
It get's worse if I want to get a reduced value from the whole stream (like average age), and a filtered list (like Persons above 18 years). It also get's worse if TeamSummary is an immutable class, and additional mutable variables are required.
In such cases it is more idiomatic to use a while loop on stream.iterator() to avoid coupling of stream methods and variables? Or is it natural to use reduce to a tuple like (oldest, accumulated).
I am aware this question is a matter of opinion unless there is an obvious way (like a special collector) that solves this elegantly.
So you want to reduce your collection to a single value? That's where Collectors.reducing comes into play (Alternative: You could use Stream.reduce but with other modifications). Furthermore, you want to aggregate your values in some way and also have the perfect accumulator: TeamSummary.
Now, in the below code I made the foollowing adjustments:
Team Summary has the merge/identity function required for reduce, as it serves as the accumulator
I use a Null Object instead of null for a non-existing person, which makes the code much more readable without null checks (NPE during converter being one of the problems). Have you thought about your output if the stream is empty?
I added a Person constructor for my own convenience. But consider using getters and final fields (even if you think getters and the whole fake encapsulation are boilerplate: You can use method references, e.g. to pass to a comparator, but not field references)
Here is the code:
static class Person {
public String firstname;
public String lastname;
public int age;
public Person(String firstname, String lastname, int age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
public static Person getNullObjectYoung() {
return new Person("", "", 0);
}
}
static class TeamSummary {
public List<String> fullnames;
public Person oldest;
public static TeamSummary merge(TeamSummary lhs, TeamSummary rhs) {
TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>();
result.fullnames.addAll(lhs.fullnames);
result.fullnames.addAll(rhs.fullnames);
result.oldest = Comparator.<Person, Integer>comparing(p -> p.age).reversed()
.compare(lhs.oldest, rhs.oldest) < 0
? lhs.oldest
: rhs.oldest;
return result;
}
public static TeamSummary of(Person person) {
TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>();
result.fullnames.add(person.firstname + " " + person.lastname);
result.oldest = person;
return result;
}
public static TeamSummary identity() {
TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>();
result.oldest = Person.getNullObjectYoung();
return result;
}
}
public static void main(String[] args) {
Stream<Person> personStream = Arrays.asList(
new Person("Tom", "T", 32),
new Person("Bob", "B", 40))
.stream();
TeamSummary result = personStream.collect(
Collectors.reducing(
TeamSummary.identity(),
TeamSummary::of,
TeamSummary::merge
));
System.out.println(result.fullnames + " " + result.oldest.age);
}
Note: You asked for a java 8 version. Maybe in java 12, you could also use Collectors.teeing, since you basically want to do two different reductions at the same time (for which we can currently leverage the accumulator).
Edit: Also added a solution for Stream.reduce, which requires a BiFunction (summary, person) -> person:
static class TeamSummary {
...
public TeamSummary include(final Person person) {
final TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>(fullnames);
result.fullnames.add(person.firstname + " " + person.lastname);
result.oldest = Comparator.<Person, Integer> comparing(p -> p.age).reversed()
.compare(oldest, person) < 0
? oldest
: person;
return result;
}
}
public static void main(final String[] args) {
...
final TeamSummary reduced = personStream.reduce(
TeamSummary.identity(),
TeamSummary::include,
TeamSummary::merge);
}
Based on the requirements such as - Stream as input and inferring the complete list of names in the output of teamSummary. You can perform the operation mapping the person and its name details to an entry and then reduce them further such as :
return personStream
.map(p -> new AbstractMap.SimpleEntry<>(p, Collections.singletonList(p.getFirstname() + ' ' + p.getLastname())))
.reduce((entry1, entry2) -> new AbstractMap.SimpleEntry<>(entry1.getKey().getAge() >= entry2.getKey().getAge() ?
entry1.getKey() : entry2.getKey(), Stream.of(entry1.getValue(), entry2.getValue()).flatMap(List::stream).collect(Collectors.toList())))
.map(entry -> new TeamSummary(entry.getKey(), entry.getValue()))
.orElseThrow(IllegalArgumentException::new);
For a readable and simplified approach though I would rather suggest passing on the collection and working with multiple stream operations here to construct the TeamSummary as :
public TeamSummary getSummary(List<Person> people) {
List<String> fullNames = people.stream()
.map(p -> p.getFirstname() + ' ' + p.getLastname())
.collect(Collectors.toList());
Person oldestPerson = people.stream()
.reduce(BinaryOperator.maxBy(Comparator.comparing(Person::getAge)))
.orElseThrow(IllegalArgumentException::new);
return new TeamSummary(oldestPerson, fullNames);
}
I don't know why you'd use Collectors.reducing() when you can stream.reduce() directly?
BinaryOperator<Player> older = (p1, p2) ->
Comparator.comparing(Player::getAge) > 0
? p1 : p2;
TeamSummary summary = stream.reduce(
TeamSummary::new, // identity
// accumulator
(ts, player) -> {
ts.addFullnames(String.format("%s %s", player.firstName, player.lastName));
ts.setOldest(older.apply(ts.getOldest(), player));
}
// combiner
(ts1, ts2) -> {
// we can safely modify the given summaries, they were all created while reducing
ts1.setOldest(Math.max(ts1.getOldest(), ts2.getOldest()));
ts1.addFullnames(ts2.getFullnames().toArray());
return ts1;
});
TeamSummary would then look like this:
class TeamSummary {
private int oldest;
public Player getOldest() { return oldest; }
public void setOldest(Player newOldest) { oldest = newOldest; }
private List<String> fullnames();
public List<String> getFullnames() { return Collections.unmodifiableList(fullnames); }
public void addFullnames(String... names) {
fullnames.addAll(Arrays.asList(names));
}
}
Alternative
You could also extend TeamSummary with something like addPlayer(Player p) and merge() to allow it to maintain its consistency:
class TeamSummary {
#Getter
private int oldest;
#Getter
private List<String> fullnames = new ArrayList<>();
public void addPlayer(Player p) {
fullnames.add(String.format("%s %s", p.getFirstname(), p.getLastname()));
oldest = olderPlayer(oldest, p);
}
public TeamSummary merge(TeamSummary other) {
older = olderPlayer(oldest, other.oldest)
fullnames.addAll(other.fullnames);
return this;
}
final static Comparator<Player> BY_AGE = Comparator.comparing(Player::getAge);
private static Player olderPlayer(Player p1, Player p2) {
return BY_AGE.compare(p1, p2) > 0 ? p1 : p2;
}
}
which would make the reduction
stream.reduce(
TeamSummary::new,
TeamSummary::addPlayer,
TeamSummary::merge
);
I am creating a map which its (key,value) will be (name, address) in my Person object:
Map<String, String> myMap = persons.stream.collect(Collector.toMap(person.getName(), person.getAddress(), (address1, address2) -> address1));
In the duplicate key situation, I would like to skip to add the second address to the map and would like to log the name also. Skipping the duplicate address I can do already using mergeFunction, but in oder to log the name I need in this mergeFunction the person object, something like:
(address1, address2) -> {
System.out.println("duplicate "+person.name() + " is found!");
return address1;
}
I am getting stuck by passing person object to this merge function.
I believe the forEach approach along with Map.merge would be much simpler and appropriate for the current use case :
Map<String, String> myMap = new HashMap<>();
persons.forEach(person -> myMap.merge(person.getName(), person.getAddress(), (adrs1, adrs2) -> {
System.out.println("duplicate " + person.getName() + " is found!");
return adrs1;
}));
Note: Map.merge also uses BiFunction (parent of BinaryOperator as used in toMap), hence you could correlate the merge function here to your existing desired functionality easily.
#Aomine: solution looks good and works for me too. Just wanted to confirm that with this it iterates twice right ?? Cause with simple solution like below it iterates only once but achieve what is required.
Map<String, String> myMap = new HashMap<>();
persons.forEach(item -> {
if(myMap.containsKey(item.getName()))
{/*do something*/}
else
myMap.put(item.getName(), item.getAddress());
});
if you want to access the whole person object in the merge function then pass Function.identity() for the valueMapper:
Map<String, Person> myMap =
persons.stream()
.collect(toMap(p -> p.getName(),
Function.identity(), // or p -> p
(p1, p2) -> { /* do logic */ }));
But as you can see the resulting map values are Person objects, if you still want a Map<String, String> as a result and still access the whole Person object in the mergeFunction then you can do the following:
persons.stream()
.collect(toMap(p -> p.getName(), Function.identity(),(p1, p2) -> { /* do logic */ }))
.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey, p -> p.getValue().getAddress()));
Here is another possibility using peek
import java.util.*;
import java.util.stream.*;
class App {
public static void main(String[] args) {
List<Person> persons = Arrays.asList(new Person("foo", "bar"), new Person("baz", "qux"), new Person("foo", "zuz"));
Set<String> names = new HashSet<>();
Map<String, String> nameAddress = persons.stream().peek(p -> {
if (names.contains(p.getName())) {
System.out.println("Duplicate key being skipped: " + p);
} else {
names.add(p.getName());
}
}).collect(Collectors.toMap(person -> person.getName(), person -> person.getAddress(), (addr1, addr2) -> addr1));
}
}
class Person {
String name;
String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
#Override
public String toString() {
return name + " " + address;
}
}
Output for me will be as follows for the snippet above:
Duplicate key being skipped: foo zuz
I have a below class and would like to transform the list of data objects into the pivot table format with java.
public class Data {
private String consultedOn;
private String consultedBy;
// Getters
// Setters
}
List<Data> reports = new ArrayList<Data>();
reports.add(new Data("04/12/2018","Mr.Bob"));
reports.add(new Data("04/12/2018","Mr.Jhon"));
reports.add(new Data("04/12/2018","Mr.Bob"));
reports.add(new Data("05/12/2018","Mr.Jhon"));
reports.add(new Data("06/12/2018","Mr.Bob"));
reports.add(new Data("06/12/2018","Mr.Jhon"));
reports.add(new Data("07/12/2018","Mr.Bob"));
I would like to transform the above list into the below table format with java within a collection.
consultedOn Mr.Bob Mr.Jhon
---------------------------------------
04/12/2018 2 1
05/12/2018 0 1
06/12/2018 1 1
07/12/2018 1 0
Note that the consultedOn field is not restricted to two values, this field may contain any data so that the collection should be dynamic.
I tried using Java8 streams with below code.
class DataMap {
private String consultedOn;
private String consultedBy;
public DataMap(String consultedOn) {
super();
this.consultedOn = consultedOn;
}
public DataMap(String consultedOn, String consultedBy) {
super();
this.consultedOn = consultedOn;
this.consultedBy = consultedBy;
}
public String getConsultedOn() {
return consultedOn;
}
public void setConsultedOn(String consultedOn) {
this.consultedOn = consultedOn;
}
public String getConsultedBy() {
return consultedBy;
}
public void setConsultedBy(String consultedBy) {
this.consultedBy = consultedBy;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((consultedOn == null) ? 0 : consultedOn.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof DataMap ))
return false;
DataMap other = (DataMap )obj;
if (consultedOn == null) {
if (other.consultedOn != null)
return false;
} else if (!consultedOn.equals(other.consultedOn))
return false;
return true;
}
}
Map<DataMap, List<DataReport>> map = reports.stream()
.collect(Collectors.groupingBy(x -> new DataMap(x.getConsultedOn(), x.getConsultedBy())));
But the map is not giving intend results as per my expectations.
I'm not sure how to go-ahead with this kind of data, any help will be appreciated.
Here's a complete answer, using the technique I explained in the comment, i.e. design a class Row representing what you want to generate for each row, i.e. a consultedOn string, and a number of consultations for each person.
public class Pivot {
private static final class Data {
private final String consultedOn;
private final String consultedBy;
public Data(String consultedOn, String consultedBy) {
this.consultedOn = consultedOn;
this.consultedBy = consultedBy;
}
public String getConsultedOn() {
return consultedOn;
}
public String getConsultedBy() {
return consultedBy;
}
}
private static final class Row {
private final String consultedOn;
private final Map<String, Integer> consultationsByPerson = new HashMap<>();
public Row(String consultedOn) {
this.consultedOn = consultedOn;
}
public void addPerson(String person) {
consultationsByPerson.merge(person, 1, Integer::sum);
}
public int getConsultationsFor(String person) {
return consultationsByPerson.getOrDefault(person, 0);
}
public String getConsultedOn() {
return consultedOn;
}
}
private static class PivotReport {
private final Map<String, Row> rowsByConsultedOn = new HashMap<>();
private SortedSet<String> persons = new TreeSet<>();
private PivotReport() {}
private void addData(Data d) {
rowsByConsultedOn.computeIfAbsent(d.getConsultedOn(), Row::new).addPerson(d.getConsultedBy());
persons.add(d.consultedBy);
}
public static PivotReport create(List<Data> list) {
PivotReport report = new PivotReport();
list.forEach(report::addData);
return report;
}
public String toString() {
String headers = "Consulted on\t" + String.join("\t", persons);
String rows = rowsByConsultedOn.values()
.stream()
.sorted(Comparator.comparing(Row::getConsultedOn))
.map(this::rowToString)
.collect(Collectors.joining("\n"));
return headers + "\n" + rows;
}
private String rowToString(Row row) {
return row.getConsultedOn() + "\t" +
persons.stream()
.map(person -> Integer.toString(row.getConsultationsFor(person)))
.collect(Collectors.joining("\t"));
}
}
public static void main(String[] args) {
List<Data> list = createListOfData();
PivotReport report = PivotReport.create(list);
System.out.println(report);
}
private static List<Data> createListOfData() {
List<Data> reports = new ArrayList<Data>();
reports.add(new Data("04/12/2018","Mr.Bob"));
reports.add(new Data("04/12/2018","Mr.Jhon"));
reports.add(new Data("04/12/2018","Mr.Bob"));
reports.add(new Data("05/12/2018","Mr.Jhon"));
reports.add(new Data("06/12/2018","Mr.Bob"));
reports.add(new Data("06/12/2018","Mr.Jhon"));
reports.add(new Data("07/12/2018","Mr.Bob"));
reports.add(new Data("07/12/2018","Mr.Smith"));
return reports;
}
}
Note that since you're using String instead of LocalDate for the consultedOn field, the dates will be sorted lexicographically instead of being sorted chronologically. You should use the appropriate type: LocalDate.
You are probably looking to use Collectors.groupingBy to group the List<DataMap> by consultedOn and further grouping it by consultedBy attribute and their count as :
Map<String, Map<String, Long>> finalMapping = reports.stream()
.collect(Collectors.groupingBy(DataMap::getConsultedOn,
Collectors.groupingBy(DataMap::getConsultedBy,Collectors.counting())));
This would provide you as an output:
{05/12/2018={Mr.Jhon=1}, 06/12/2018={Mr.Jhon=1, Mr.Bob=1},
07/12/2018={Mr.Bob=1}, 04/12/2018={Mr.Jhon=1, Mr.Bob=2}}
Further, if you require all the corresponding consultedBy values to be accounted in, you can create a Set of those from the initial List<DataMap> as :
Set<String> consultedBys = reports.stream()
.map(DataMap::getConsultedBy)
.collect(Collectors.toSet());
using which you can modify your existing map obtained to contain 0 count as well in the following manner:
finalMapping.forEach((k, v) -> consultedBys.forEach(c -> v.putIfAbsent(c, 0L)));
This would now provide you as the output:
{05/12/2018={Mr.Jhon=1, Mr.Bob=0}, 06/12/2018={Mr.Jhon=1, Mr.Bob=1},
07/12/2018={Mr.Jhon=0, Mr.Bob=1}, 04/12/2018={Mr.Jhon=1, Mr.Bob=2}}
The other would be like this:
Map<Pair<String, String>, Integer> map = reports
.stream()
.collect(toMap(data -> new Pair<>(data.getConsultedOn(),
data.getConsultedBy()), data -> 1, Integer::sum));
Map<String, DataMap> result= new HashMap<>();
-
class DataMap {
private String consultedOn;
private Map<String, Integer> map;
}
-
Set<String> persons = new HashSet<>();
persons = reports.stream().map(Data::getConsultedBy).collect(Collectors.toSet());
-
for (Map.Entry<Pair<String, String>, Integer> entry : map.entrySet()) {
Map<String, Integer> val = new HashMap<>();
for (String person : persons) {
if (!person.equals(entry.getKey().getValue()))
val.put(person, 0);
else
val.put(entry.getKey().getValue(), entry.getValue());
}
result.put(entry.getKey().getKey(), new DataMap(entry.getKey().getKey(), val));
}
and final result:
List<DataMap> finalResult = new ArrayList<>(result.values());
Instead of using a separate data structure, you can use a Map of key as consultedOn (date or String) and have the value as a list of (String or your own defined POJO with overridden equals() method.Here in I have used a map like Map<String, List<String>>
All you need is the two methods:
one to set report (addDataToReport) : for each consultedOn (key), create a list of doctors consulted . See comments for map.merge usage
and one to display the data in a report manner (printReport). We are using "%10s" to give proper formatting. Instead of println, format doesn't implicitly append a new line character
Moreover to get the report's column we need to have a set (unique value list), doctors.add(consultedBy); will serve us for this purpose . Java will take care of keeping the doctors' value unique.
public class Application {
Set<String> doctors = new LinkedHashSet<>();
private void addDataToReport(Map<String, List<String>> reportMap, String consultedOn, String consultedBy) {
doctors.add(consultedBy); // set the doctors Set
reportMap.merge(consultedOn, Arrays.asList(consultedBy)// if key = consultedOn is not there add , a new list
, (v1, v2) -> Stream.concat(v1.stream(), v2.stream()).collect(Collectors.toList()));//else merge previous and new values , here concatenate two lists
}
private void printReport(Map<String, List<String>> reportMap) {
/*Set Headers*/
String formatting = "%10s";//give a block of 10 characters for each string to print
System.out.format(formatting, "consultedOn");
doctors.forEach(t -> System.out.format(formatting, t));// print data on console without an implicit new line
System.out.println("\n---------------------------------------");
/*Set row values*/
for (Map.Entry<String, List<String>> entry : reportMap.entrySet()) {
Map<String, Integer> map = new LinkedHashMap<>();
doctors.forEach(t -> map.put(t, 0)); // initialise each doctor count on a day to 0
entry.getValue().forEach(t -> map.put(t, map.get(t) + 1));
System.out.format(formatting, entry.getKey());
map.values().forEach(t -> System.out.format(formatting, t));
System.out.println();
}
}
public static void main(String[] args) {
Application application = new Application();
Map<String, List<String>> reportMap = new LinkedHashMap<>();
String MR_JHON = "Mr.Jhon";
String MR_BOB = "Mr.Bob ";
application.addDataToReport(reportMap, "04/12/2018", MR_BOB);
application.addDataToReport(reportMap, "04/12/2018", MR_JHON);
application.addDataToReport(reportMap, "04/12/2018", MR_BOB);
application.addDataToReport(reportMap, "05/12/2018", MR_JHON);
application.addDataToReport(reportMap, "06/12/2018", MR_BOB);
application.addDataToReport(reportMap, "06/12/2018", MR_JHON);
application.addDataToReport(reportMap, "07/12/2018", MR_BOB);
application.printReport(reportMap);
}
}
Result
consultedOn Mr.Bob Mr.Jhon
---------------------------------------
04/12/2018 2 1
05/12/2018 0 1
06/12/2018 1 1
07/12/2018 1 0
I have something like the below :
public class MyClass {
private Long stackId
private Long questionId
}
A collection of say 100, where the stackid could be duplicate with different questionIds. Its a one to many relationship between stackId and questionId
Is there a streamy, java 8 way to convert to the below strcuture :
public class MyOtherClass {
private Long stackId
private Collection<Long> questionIds
}
Which would be a collection of 25, with each instance having a nested collection of 4 questionIds.
Input :
[{1,100},{1,101},{1,102},{1,103},{2,200},{2,201},{2,202},{1,203}]
Output
[{1, [100,101,102,103]},{2,[200,201,202,203]}]
The straight-forward way with the Stream API involves 2 Stream pipelines:
The first one creates a temporary Map<Long, List<Long>> of stackId to questionIds. This is done with the groupingBy(classifier, downstream) collectors where we classify per the stackId and values having the same stackId are mapped to their questionId (with mapping) and collected into a list with toList().
The second one converts each entry of that map into a MyOtherClass instance and collects that into a list.
Assuming you have a constructor MyOtherClass(Long stackId, Collection<Long> questionIds), a sample code would be:
Map<Long, List<Long>> map =
list.stream()
.collect(Collectors.groupingBy(
MyClass::getStackId,
Collectors.mapping(MyClass::getQuestionId, Collectors.toList())
));
List<MyOtherClass> result =
map.entrySet()
.stream()
.map(e -> new MyOtherClass(e.getKey(), e.getValue()))
.collect(Collectors.toList());
Using StreamEx library, you could do that in a single Stream pipeline. This library offers a pairing and first collectors. This enables to pair two collectors and perform a finisher operation on the two collected results:
The first one only keeps the first stackId of the grouped elements (they will all be the same, by construction)
The second one mapping each element into their questionId and collecting into a list.
The finisher operation just returns a new instance of MyOtherClass.
Sample code:
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static one.util.streamex.MoreCollectors.first;
import static one.util.streamex.MoreCollectors.pairing;
// ...
Collection<MyOtherClass> result =
StreamEx.of(list)
.groupingBy(
MyClass::getStackId,
pairing(
collectingAndThen(mapping(MyClass::getStackId, first()), Optional::get),
mapping(MyClass::getQuestionId, toList()),
MyOtherClass::new
)
).values();
List<MyClass> inputs = Arrays.asList(
new MyClass(1L, 100L),
new MyClass(1L, 101L),
new MyClass(1L, 102L),
new MyClass(1L, 103L),
new MyClass(2L, 200L),
new MyClass(2L, 201L),
new MyClass(2L, 202L),
new MyClass(2L, 203L)
);
Map<Long, List<Long>> result = inputs
.stream()
.collect(
Collectors.groupingBy(MyClass::getStackId,
Collectors.mapping(
MyClass::getQuestionId,
Collectors.toList()
)
)
);
You can use the java8 groupingBy collector. Like this:
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class RandomTest {
class MyClass {
private Long stackId;
private Long questionId;
public MyClass(Long stackId, Long questionId) {
this.stackId = stackId;
this.questionId = questionId;
}
public Long getStackId() {
return stackId;
}
public Long getQuestionId() {
return questionId;
}
}
public class MyOtherClass {
private Long stackId;
private Set<Long> questionIds;
public MyOtherClass(Long stackId, Set<Long> questionIds) {
this.stackId = stackId;
this.questionIds = questionIds;
}
public Long getStackId() {
return stackId;
}
public Set<Long> getQuestionIds() {
return questionIds;
}
}
#Test
public void test() {
List<MyClass> classes = new ArrayList<>();
List<MyOtherClass> otherClasses = new ArrayList<>();
//populate the classes list
for (int j = 1; j <= 25; j++) {
for (int i = 0; i < 4; i++) {
classes.add(new MyClass(0L + j, (100L*j) + i));
}
}
//populate the otherClasses List
classes.stream().collect(Collectors
.groupingBy(MyClass::getStackId, Collectors.mapping(MyClass::getQuestionId, Collectors.toSet())))
.entrySet().stream().forEach(
longSetEntry -> otherClasses.add(new MyOtherClass(longSetEntry.getKey(), longSetEntry.getValue())));
//print the otherClasses list
otherClasses.forEach(myOtherClass -> {
System.out.print(myOtherClass.getStackId() + ": [");
myOtherClass.getQuestionIds().forEach(questionId-> System.out.print(questionId + ","));
System.out.println("]");
});
}
}