I have the data which I am reading from CSV in below format.
accountId, recordType, amount
1, past, 40
1, past, 40
1, present, 60
2, past, 20
2, present, 10
2, present, 60
Whats the simplest way I can process this to group by account id and recordType and find average on amount. I know it can be done with structures, multiple hash maps etc which make code look ugly
Expected output
accountId, recordType, amount
1, past, 40
1, present, 60
2, past, 20
2, present, 35
Here is what i tried, its incomplete, but thats the approach i was not happy about
//Map to store count of accountId to events
Map<String, Float> countHistory = new HashMap<String, Float>();
Map<String, Float> countPresent = new HashMap<String, Float>();
//Map to store count of accountId to sum of instance launched
Map<String, Float> amountPresent = new HashMap<String, Float>();
Map<String, Float> amountHistory = new HashMap<String, Float>();
for(LaunchEvent e : inputList) {
if(e.getDataset().equalsIgnoreCase("history")) {
countHistory.put(e.getAccountId(), amountHistory.getOrDefault(e.getAccountId(), 0.0f) + 1.0f);
amountHistory.put(e.getAccountId(), amountHistory.getOrDefault(e.getAccountId(), 0.0f) + Float.valueOf(e.getAmount()));
} else {
amountPresent.put(e.getAccountId(), amountPresent.getOrDefault(e.getAccountId(), 0.0f) + 1.0f);
amountPresent.put(e.getAccountId(), amountPresent.getOrDefault(e.getAccountId(), 0.0f) + Float.valueOf(e.getAmount()));
}
}
The key is to use Java as an OO language, where you can define classes and objects.
Each row is an account with three fields. So let's define a class Account that has these three fields.
You want to group accounts by a key, composed of two fields of the account. And if the two fields are equal, then the keys should be equal. So let's define a class AccountGroupingKey which represents that key and properly overrides equals() and hashCode().
For each key, you want the average of the amounts of the accounts having that key. So you want a Map<AccountGroupingKey, Double>.
How to create this map? By using the groupingBycollector, since you want to... group the accounts by key. And we'll use the averagingInt collector to transform each group of accounts into an average of integers.
So in the end, all you need is the following.
It might look verbose, but if you omit the autogenerated getters, equals and hashCode and concentrate on the logic, it's actually extremely succinct and readable.
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class AccountGroupingAndAveraging {
static class Account {
private final int id;
private final String type;
private final int amount;
public Account(int id, String type, int amount) {
this.id = id;
this.type = type;
this.amount = amount;
}
public int getId() {
return id;
}
public String getType() {
return type;
}
public int getAmount() {
return amount;
}
}
static class AccountGroupingKey {
private final int id;
private final String type;
public AccountGroupingKey(Account account) {
this.id = account.getId();
this.type = account.getType();
}
public int getId() {
return id;
}
public String getType() {
return type;
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AccountGroupingKey that = (AccountGroupingKey) o;
return id == that.id &&
Objects.equals(type, that.type);
}
#Override
public int hashCode() {
return Objects.hash(id, type);
}
}
public static void main(String[] args) {
List<Account> accounts = Arrays.asList(
new Account(1, "past", 40),
new Account(1, "past", 40),
new Account(1, "present", 60),
new Account(2, "past", 20),
new Account(2, "present", 10),
new Account(2, "present", 60)
);
Map<AccountGroupingKey, Double> result =
accounts.stream().collect(
Collectors.groupingBy(AccountGroupingKey::new,
Collectors.averagingInt(Account::getAmount)));
result.forEach((key, average) -> System.out.println(key.id + ", " + key.type + ", " + average));
}
}
Thanks to JB Nizet. I took his idea and simplified the solution without needing extra class.
Map<String, Map<String, Double>> res = events.stream()
.collect(Collectors.groupingBy(LaunchEvent::getAccountId,
Collectors.groupingBy(LaunchEvent::getRecordType,
Collectors.averagingDouble(LaunchEvent::getAmount))));
This will produce the output
Result {1={past=7.5, present=15.0}, 2={past=10.0, present=35.0}}
Related
I have a method that sorts a List by different criteria and returns the name (an instance variable) of the one with maximum value. In case more than one instance is having the maximum, all of their names should be concatenated.
Let's say I have Class A as follows.
Class A {
...
String getName(){...}
int getValue1() {...}
int getValue2() {...}
...
int getValueN() {...}
...
}
I have a List<A> listToSort. I would normally sort this list as listToSort.sort(Comparator.comparing(A::getValue1)) or listToSort.sort(Comparator.comparing(A::getValue2)), so on and so forth. Then get the ones sharing the maximum value.
In a method I believe this should be done as:
String getMaxString (Comparator c) {
listToSort.sort(c);
...
}
and send Comparator.comparing(A.getValueX) as parameter to call it with different methods. (X here indicates an arbitrary number for the getValue function).
However, I need to also return other instances sharing the same values
I will need to pass the Class methods to my method and call on instances as:
String getMaxString (Comparator c) {
listToSort.sort(c);
int maxValue = listToSort.get(listToSort.size() - 1).getValueX();
String maxString = listToSort.get(listToSort.size() - 1).getName();
for (int i = listToSort.size() - 2; i >= 0; i--) {
if (listToSort.get(i).getValueX()() == maxValue) {
maxString += ", " + listToSort.get(i).getName();
}
}
return maxString;
}
How would I pass this method to call on instances here? Or do I need to consider another way?
Edit:
I have a list of Courses as List<Course> mylist where a course can be simplified as:
Class Course {
private String name;
private int capacity;
private int students;
...
//bunch of getters.
}
My task is to return Strings for the course(es) with maximum capacity, the course(es) with maximum registered students, the course(es) with most difficulty, the maximum filled percentage, the course(es) with the maximum number of TAs etc...
Edit 2:
As requested in the comment section.
List of
Course a (name "a", capacity 10, students 5)
Course b (name "b", capacity 20, students 5)
Course c (name "c", capacity 30, students 0)
Sorting based on capacity should return "c"
Sorting based on students should return "a b"
You can pass the getter method and create the Comparator in getMaxString:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
public class Foo {
static class AClass {
private final String name;
private final int value1;
private final int value2;
String getName() { return name; }
int getValue1() { return value1; }
int getValue2() { return value2; }
public AClass(String name, int value1, int value2) {
this.name = name;
this.value1 = value1;
this.value2 = value2;
}
}
static String getMaxString(Function<AClass,Integer> f, List<AClass> listToSort) {
listToSort.sort(Comparator.comparing(f));
int maxValue = f.apply(listToSort.get(listToSort.size() - 1));
String maxString = listToSort.get(listToSort.size() - 1).getName();
for (int i = listToSort.size() - 2; i >= 0; i--) {
if (f.apply(listToSort.get(i)) == maxValue) {
maxString += ", " + listToSort.get(i).getName();
}
}
return maxString;
}
public static void main(String[] args) {
List<AClass> list = new ArrayList<>();
list.add(new AClass("a", 1,2));
list.add(new AClass("b", 1,2));
list.add(new AClass("c", 2,1));
list.add(new AClass("d", 2,1));
System.out.println(getMaxString(AClass::getValue1, list));
System.out.println(getMaxString(AClass::getValue2, list));
}
}
As Tim Moore suggested above, it isn't necessary to sort the list (which has cost O(n*log n)), we can traverse it twice:
static String getMaxString2(ToIntFunction<AClass> f, List<AClass> listToSort) {
int maxValue = listToSort.stream().mapToInt(f).max().orElseThrow();
return listToSort.stream()
.filter(a -> maxValue == f.applyAsInt(a))
.map(AClass::getName)
.collect(Collectors.joining(", "));
}
Note that you should test your code with an empty list.
It's useful to look at the type signature for Comparator.comparing, because it sounds like you want to do something similar:
static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor)
The interesting part is the type of keyExtractor. Roughly speaking, it's a function from the type of the object you're comparing, to the type of the field you want to use for the comparison. In our case, these correspond to the A class and Integer. Because these types are fixed in this example, you can declare a method with a signature like this:
String getMaxString(Function<A, Integer> property)
With the existing algorithm, it can be used this way:
String getMaxString(Function<A, Integer> property) {
listToSort.sort(Comparator.comparing(property));
int maxValue = property.apply(listToSort.get(listToSort.size() - 1));
String maxString = listToSort.get(listToSort.size() - 1).getName();
for (int i = listToSort.size() - 2; i >= 0; i--) {
if (listToSort.get(i).getValueN() == maxValue) {
maxString += ", " + listToSort.get(i).getName();
}
}
return maxString;
}
However, it isn't necessary or efficient to sort the entire list in order to determine the maximum elements, as this can be determined by iterating through the list once:
String getMaxString(Function<A, Integer> property) {
int maxValue = Integer.MIN_VALUE;
StringBuilder maxString = new StringBuilder();
for (A element : listToSort) {
int currentValue = property.apply(element);
if (currentValue > maxValue) {
// there is a new maximum, so start the string again
maxString = new StringBuilder(element.getName());
maxValue = currentValue;
} else if (currentValue == maxValue) {
// equal to the existing maximum, append it to the string
if (maxString.length() > 0) {
maxString.append(", ");
}
maxString.append(element.getName());
}
// otherwise, it's less than the existing maximum and can be ignored
}
return maxString.toString();
}
Either way, you can call it using the same method reference syntax:
getMaxString(A::getValueN)
Time complexity O(n) - only one iteration through the dataset.
Hope it'll help.
If something will be unclear fill free to raise a question.
Main
public class MaxClient {
public static void main(String[] args) {
Comparator<A> comp = Comparator.comparingInt(A::getVal1);
List<A> items = List.of(new A(1, 8), new A(2, 8), new A(5, 8), new A(5, 27), new A(3, 8));
items.stream()
.collect(new GetMax(comp))
.forEach(System.out::println);
}
}
Custom collector GetMax
public class GetMax implements Collector <A, Deque<A>, Deque<A>> {
private final Comparator<A> comp;
public GetMax(Comparator<A> comp) {
this.comp = comp;
}
#Override
public Supplier<Deque<A>> supplier() {
return ArrayDeque::new;
}
#Override
public BiConsumer<Deque<A>, A> accumulator() {
return (stack, next) -> {
if (!stack.isEmpty() && comp.compare(next, stack.peekFirst()) > 0) stack.clear();
if (stack.isEmpty() || comp.compare(next, stack.peekFirst()) == 0) stack.offerLast(next);
};
}
#Override
public BinaryOperator<Deque<A>> combiner() {
return (stack1, stack2) -> {
if (stack1.isEmpty()) return stack2;
if (stack2.isEmpty()) return stack1;
if (comp.compare(stack1.peekFirst(), stack2.peekFirst()) == 0) {
stack1.addAll(stack2);
}
return stack1;
};
}
#Override
public Function<Deque<A>, Deque<A>> finisher() {
return stack -> stack;
}
#Override
public Set<Characteristics> characteristics() {
return Set.of(Characteristics.UNORDERED);
}
}
Class A that I used for testing purposes
public class A {
private int val1;
private int val2;
public A(int val1, int val2) {
this.val1 = val1;
this.val2 = val2;
}
public int getVal1() {
return val1;
}
public int getVal2() {
return val2;
}
#Override
public String toString() {
return "A val1: " + val1 + " val2: " + val2;
}
}
OUTPUT
A val1: 5 val2: 8
A val1: 5 val2: 27
Thanks for posting the information I requested. Here is what I came up with.
Create a list of Course objects
List<Course> list = List.of(
new Course("a", 10, 5),
new Course("b", 20, 5),
new Course("c", 30, 0));
Stream the methods and apply them to the list
List<String> results = Stream.<Function<Course, Integer>>of(
Course::getCapacity,
Course::getStudents)
.map(fnc-> getMaxString(fnc, list))
.toList();
results.forEach(System.out::println);
print the results
c
a b
I wrote a simple method that takes a method reference and list and finds the maximum. It does not do any sorting.
allocate a list to hold the names
set the maximum to the lowest possible
iterate thru the list applying the method.
if the value is greater than the current max replace it and clear the current list of names.
otherwise, if equal, add a new name.
once done, return the formatted string.
static String getMaxString(Function<Course, Integer> fnc,
List<Course> list) {
List<String> result = new ArrayList<>();
int max = Integer.MIN_VALUE;
for (Course obj : list) {
int val = fnc.apply(obj);
if (val >= max) {
if (val > max) {
result.clear();
}
max = val;
result.add(obj.getName());
}
}
return String.join(" ", result);
}
I have a CSV file with datapoints as
student, year, subject, score1, score2, score3, ..., score100
Alex, 2010, Math, 23, 56, 43, ..., 89
Alex, 2011, Science, 45, 32, 45, ..., 65
Matt, 2009, Art, 34, 56, 75, ..., 43
Matt, 2010, Math, 43, 54, 54, ..., 32
What would be the best way to load such CSV as Map in Java. This data is used for lookup service hence the chosen map data structure. The key would be the Tuple (student, year) -> which returns a list of subject + scores (SubjectScore.class). So the idea is given the name of the student and year, get all subjects and scores.
I didn't find an elegant solution while searching to read the CSV file in a Map of defined classes like Map<Tuple, List<SubjectScore>>
class Tuple {
private String student;
private int year;
}
class SubjectScore {
private String subject;
private int score1;
private int score2;
private int score3;
// more fields here
private int score100;
}
Additional details: The CSV file is large ~ 2 GB but is static in nature, hence deciding to load in memory.
Please find below a first example, which may serve as a starting point. I have removed the dots in your example input data and assume a simplified example with 4 scores.
student, year, subject, score1, score2, score3, ..., score100
Alex, 2010, Math, 23, 56, 43, 89
Alex, 2011, Science, 45, 32, 45, 65
Matt, 2009, Art, 34, 56, 75, 43
Matt, 2010, Math, 43, 54, 54, 32
Alex, 2010, Art, 43, 54, 54, 32
I also assume that you have overwritten the equals and hashcode methods in your tuple class and implemented a suitable constructor
class Tuple {
private String student;
private int year;
public Tuple(String student, int year) {
this.student = student;
this.year = year;
}
#Override
public int hashCode() {
int hash = 7;
hash = 79 * hash + Objects.hashCode(this.student);
hash = 79 * hash + this.year;
return hash;
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Tuple other = (Tuple) obj;
if (this.year != other.year) {
return false;
}
return Objects.equals(this.student, other.student);
}
#Override
public String toString() {
return "Tuple{" + "student=" + student + ", year=" + year + '}';
}
}
and a SubjectScore class with a suitable constructor
class SubjectScore {
private String subject;
private int score1;
private int score2;
private int score3;
// more fields here
private int score4;
public SubjectScore(String row) {
String[] data = row.split(",");
this.subject = data[0];
this.score1 = Integer.parseInt(data[1].trim());
this.score2 = Integer.parseInt(data[2].trim());
this.score3 = Integer.parseInt(data[3].trim());
this.score4 = Integer.parseInt(data[4].trim());
}
}
Then you can create the desired map as follows:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Example {
public static void main(String[] args) {
Map<Tuple, List<SubjectScore>> map = new HashMap<>();
try (Stream<String> content = Files.lines(Paths.get("path to your csv file"))) {
map = content.skip(1).map(line -> lineToEntry(line)) //skip header and map each line to a map entry
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList()))
);
} catch (IOException ex) {
ex.printStackTrace();
}
map.forEach((k,v) -> {System.out.println(k + " : " + v);});
}
static Entry<Tuple, SubjectScore> lineToEntry(String line) {
//split each line at the first and second comma producing an array with 3 columns
// first column with the name and second with year to create a tuple object
// evrything after the second comma as one column to create a SubjectScore object
String[] data = line.split(",", 3);
Tuple t = new Tuple(data[0].trim(), Integer.parseInt(data[1].trim()));
SubjectScore s = new SubjectScore(data[2]);
return new AbstractMap.SimpleEntry<>(t, s);
}
}
I don't know if you really need individual fields for each score in your SubjectScore class. If I were you, I would prefer a list of integers. To do so just change your class to something like :
class SubjectScore {
private String subject;
private List<Integer> scores;
public SubjectScore(String row) {
String[] data = row.split(",");
this.subject = data[0];
this.scores = Arrays.stream(data, 1, data.length)
.map(item -> Integer.parseInt(item.trim()))
.collect(Collectors.toList());
}
}
I was wondering how to take the same approach but convert it into
Map<String, Map<Integer, List<SubjectScore>>>.
I have decided to add another answer because your needs regarding the data type have changed. Assuming you have still the same SubjectScore class
class SubjectScore {
private String subject;
private List<Integer> scores;
public SubjectScore(String row) {
String[] data = row.split(",");
this.subject = data[0];
this.scores = Arrays.stream(data, 1, data.length)
.map(item -> Integer.parseInt(item.trim()))
.collect(Collectors.toList());
}
}
The old fashioned way with if-else blocks to check if a key-value pair alreday exists:
public static void main(String[] args) throws IOException {
List<String> allLines = Files.readAllLines(Paths.get("path to your file"));
Map<String,Map<String, List<SubjectScore>>> mapOldWay = new HashMap<>();
for(String line : allLines.subList(1, allLines.size())){
//split each line in 3 parts, i.e 1st column, 2nd column and everything after 3rd column
String data[] = line.split("\\s*,\\s*",3);
if(mapOldWay.containsKey(data[0])){
if(mapOldWay.get(data[0]).containsKey(data[1])){
mapOldWay.get(data[0]).get(data[1]).add(new SubjectScore(data[2]));
}
else{
mapOldWay.get(data[0]).put(data[1], new ArrayList<>());
mapOldWay.get(data[0]).get(data[1]).add(new SubjectScore(data[2]));
}
}
else{
mapOldWay.put(data[0], new HashMap<>());
mapOldWay.get(data[0]).put(data[1], new ArrayList<>());
mapOldWay.get(data[0]).get(data[1]).add(new SubjectScore(data[2]));
}
}
printMap(mapOldWay);
}
public static void printMap(Map<String, Map<String, List<SubjectScore>>> map) {
map.forEach((outerkey,outervalue) -> {
System.out.println(outerkey);
outervalue.forEach((innerkey,innervalue)-> {
System.out.println("\t" + innerkey + " : " + innervalue);
});
});
}
Same logic but shorter using java 8 features (Map#computeIfAbsent):
public static void main(String[] args) throws IOException {
List<String> allLines = Files.readAllLines(Paths.get("path to your file"));
Map<String,Map<String, List<SubjectScore>>> mapJ8Features = new HashMap<>();
for(String line : allLines.subList(1, allLines.size())){
String data[] = line.split("\\s*,\\s*",3);
mapJ8Features.computeIfAbsent(data[0], k -> new HashMap<>())
.computeIfAbsent(data[1], k -> new ArrayList<>())
.add(new SubjectScore(data[2]));
}
}
Another approach using streams and nested Collectors#groupingBy
public static void main(String[] args) throws IOException {
Map<String,Map<String, List<SubjectScore>>> mapStreams = new HashMap<>();
try (Stream<String> content = Files.lines(Paths.get("path to your file"))) {
mapStreams = content.skip(1).map(line -> line.split("\\s*,\\s*",3))
.collect(Collectors.groupingBy(splited -> splited[0],
Collectors.groupingBy(splited -> splited[1],
Collectors.mapping(splited -> new SubjectScore(splited[2]),Collectors.toList()))));
} catch (IOException ex) {
ex.printStackTrace();
}
}
Note: I'm just now realizing that you wanted to represent the year as an Integer. I left it as string. If you want to change it just replace everywhere data[1] or splited[1] with Integer.parseInt(data[1] or splited[1])
I have a object list, which I need to group by 2 different atributes and then sum the values of an attribute, the structure of my object is something like this:
private Long id1;
private Long id2;
private Double amountReserved;
private Double amountRequired;
//... more atributes and getters and setters
So, I then have a list, for example:
List<MyList> list = Arrays.asList(
list(1, A, 50, 200)
list(1, A, 50, 200)
list(1, B, 0, 100)
list(2, A, 10, 15)
list(2, A, 5, 15)
list(3, A, 0, 25));
What I am trying to achieve is a new list with the below structure:
list(1, A, 100, 100)
list(1, B, 0, 100)
list(2, A, 15, 0)
list(3, A, 0, 25)
Elucidating what is the requisite I am trying to achieve:
Group objects by id1 and id2
sum the amountReservedof the grouped object
subtract amountRequired from the summed amountReserved
What I have so far:
This one got me the groupings as I wanted
Map<Long, Map<String, List<MyList>>> map = null;
map = lista.stream().collect(Collectors.groupingBy(PreSeparacaoDto::getCodigoPedido,
Collectors.groupingBy(PreSeparacaoDto::getCodigoProduto)));
This one sums by group id1, but I am struggling to add the second groupingby on it, as I get syntax errors:
lista.stream()
.collect(Collectors.groupingBy(PreSeparacaoDto::getCodigoPedido,
Collectors.summingDouble(PreSeparacaoDto::getProdutoQuantidadeSeparada)))
.forEach((codigoPedido, ProdutoQuantidadeSeparada) -> System.out.println( codigoPedido + ": " + ProdutoQuantidadeSeparada ));
My problem is that I failed to get those together ( as per requisite 2) and was not even close to achieve my requisite 3.
I tried to use reduction, as explained here , but honestly, I was not able to replicate it with a single grouping, the reducing is returning an error informing that my parameters don't meet the reducing parameters. I looked for some other options here on stackoverflow and other websites, but without success.
Can someone help me out and poiting where I am failing to combine the reduction with my group, or if that is the correct path I should be following.
I think an easy way is to use Collectors.grouping : you tell it how to group and what to collect.
Here's an example, computing only the sum of AmountReserved :
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class GroupedSums {
static class MyList {
Long id1;
char id2;
Double amountReserved;
Double amountRequired;
public Long getId1() {
return id1;
}
public char getId2() {
return id2;
}
public Double getAmountReserved() {
return amountReserved;
}
public Double getAmountRequired() {
return amountRequired;
}
public MyList(Long id1, char id2, Double amountReserved, Double amountRequired) {
super();
this.id1 = id1;
this.id2 = id2;
this.amountReserved = amountReserved;
this.amountRequired = amountRequired;
}
Key key() {
return new Key(id1, id2);
}
}
private static MyList list(Long id1, char id2, Double amountReserved, Double amountRequired) {
return new MyList(id1, id2, amountReserved, amountRequired);
}
public GroupedSums() {
}
private static class Key {
Long id1;
char id2;
public Long getId1() {
return id1;
}
public char getId2() {
return id2;
}
public Key(Long id1, char id2) {
super();
this.id1 = id1;
this.id2 = id2;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id1 == null) ? 0 : id1.hashCode());
result = prime * result + id2;
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Key other = (Key) obj;
if (id1 == null) {
if (other.id1 != null)
return false;
} else if (!id1.equals(other.id1))
return false;
if (id2 != other.id2)
return false;
return true;
}
#Override
public String toString() {
return "[id1=" + id1 + ", id2=" + id2 + "]";
}
}
public static void main(String[] args) {
List<MyList> list = Arrays.asList(
list(1L, 'A', 50d, 200d),
list(1L, 'A', 50d, 200d),
list(1L, 'B', 0d, 100d),
list(2L, 'A', 10d, 15d),
list(2L, 'A', 5d, 15d),
list(3L, 'A', 0d, 25d));
list.stream().collect(Collectors.groupingBy(MyList::key, Collectors.summingDouble(MyList::getAmountReserved)))
.forEach((k,v)->System.out.println("" + k + " :" + v));
}
}
HTH!
You might just be looking for simply Collectors.toMap as :
List<MyList> output = new ArrayList<>(lista.stream()
.collect(Collectors.toMap(a -> a.getId1() + "-" + a.getId2(), a -> a, (myList1, myList2) -> {
myList1.amountReserved = myList1.amountReserved + myList2.amountReserved;
myList1.amountRequired = myList1.amountRequired - myList1.amountReserved;
return myList1;
})).values());
You can stream over the input list twice.
First time, you group by id1, id2 and compute the sum of amount reserved. Second time, you can stream the list again, group it (by id1 and id2) by making use of the above result to find the difference.
Map<Long, Map<Long, Double>> amountReservedGroup = list.stream()
.collect(Collectors.groupingBy(MyList::getId1, Collectors.groupingBy(MyList::getId2,
Collectors.summingDouble(MyList::getAmountReserved))));
Map<Long, Map<Long, List<MyList>>> finalResult = list.stream()
.collect(Collectors.groupingBy(MyList::getId1, Collectors.groupingBy(MyList::getId2,
Collectors.mapping(o -> new MyList(o.getId1(), o.getId2(),
amountReservedGroup.get(o.getId1()).get(o.getId2()),
o.getAmountRequired() - amountReservedGroup.get(o.getId1()).get(o.getId2())),
Collectors.toList()))));
Note:
This does not handle the case when the result of the subtraction is negative!!
As pointed out by nullpointer# in the comments, will the value of amountRequired be the same for a given id1 and id2?
you can do order by id1 and then order id2 (to make sure elements of the same list and sublist are after each other) and then you do nested foreach (before you iterate the sublist, you init result_reserved_amount to 0 and result_required_amount to the initial value)
then you do if same ids (if id1= previous_id1 and id2 = previous_id2) do result_reserved_amount+= current_reserved_amount and result_required_amount -= current_reserved_amount, otherwise update previous_id1, previous_id2, result_reserved_amount, result_required_amount
We have
List<persons> persons;
and we need
Map<age,Map<income,Person>> results
the way I am doing it now is :
ages.stream().forEach(a -> {
Map<Integer,Person> tmpMap = new HashMap<>();
incomes.stream().forEach(i -> {
Person p = persons.stream().filter(
u -> u.getAge() == a.getAge() &&
u.getIncome() == i.getIncome())
.findAny().orElse(null);
tmpMap.put(i.getIncome(), p);
});
returns.put(a.getAge(),tmpMap);
});
it seems like there should be a better way of doing this.
This looks like it works.
List<Person> people = Arrays.asList(
new Person("One", 21, 100),
new Person("Two", 21, 75),
new Person("Three", 42, 100),
new Person("Four", 42, 120),
new Person("Five", 9, 100)
);
Map<Integer, Map<Integer, Person>> map = people.stream()
// Gather all ages into a Map<Age,List<Person>>
.collect(Collectors.groupingBy(Person::getAge))
// Walk that transient Map.
.entrySet().stream()
.collect(Collectors.toMap(
// Key is the age.
Map.Entry::getKey,
// Value is a Map<income,person>
e -> e.getValue()
// Roll each of the same age into a Map<Income,Person>
.stream().collect(
Collectors.toMap(
// Key is income.
Person::getIncome,
// Value is the Person.
Function.identity()
))));
I roll your list into a Map<Age,List<Person>> using a groupingBy and then stream it's entrySet and collect that into the final form.
This will fail if two people of the same age have the same income because that will violate the inner Map. Use Alexander's suggestion if you are happy with the natural enhancement of generating a Map<Integer, Map<Integer, List<Person>>>.
Added
#Holger has pointed out in a comment that this can be done in a much simpler and more elegant way. Please use this form instead/
Map<Integer, Map<Integer, Person>> map2 = people.stream()
.collect(
Collectors.groupingBy(
Person::getAge,
Collectors.toMap(Person::getIncome, Function.identity())));
FYI - Here's the Person class I used. Note the equals and hashcode are implemented.
class Person {
private final String name;
private final int age;
private final int income;
public Person(String name, int age, int income) {
this.name = name;
this.age = age;
this.income = income;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public int getIncome() {
return income;
}
#Override
public String toString() {
return "Person{" + "name=" + name + ", age=" + age + ", income=" + income + '}';
}
#Override
public int hashCode() {
int hash = 7;
hash = 59 * hash + Objects.hashCode(this.name);
hash = 59 * hash + this.age;
hash = 59 * hash + this.income;
return hash;
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Person other = (Person) obj;
if (this.age != other.age) {
return false;
}
if (this.income != other.income) {
return false;
}
if (!Objects.equals(this.name, other.name)) {
return false;
}
return true;
}
}
This should do the trick
List<person> persons = Arrays.asList(
new person(22, 1000),
new person(25, 1500),
new person(22, 2500),
new person(32, 5000)
);
Map<Integer, Map<Integer, List<person>>> map = persons.stream().collect(
groupingBy(person::getAge, groupingBy(person::getIncome))
);
System.out.println(map);
Output:
{32={5000=[person{age=32, income=5000}]}, 22={2500=[person{age=22, income=2500}], 1000=[person{age=22, income=1000}]}, 25={1500=[person{age=25, income=1500}]}}
NB: The result is not exactly what you expect as you will get a Map<Integer, Map<Integer, List<person>>> instead of Map<Integer, Map<Integer, person>> but I assume that your initial question is not correct because if you have two persons with the same age and income, you will have only one person in your map instead of two
You should take a look at Collectors.groupingBy():
List<Person> persons = new ArrayList<>();
Map<Integer, Map<Integer, List<Person>>> map = persons.stream().collect(Collectors.groupingBy(person -> person.getAge(),Collectors.groupingBy(person -> person.getIncome())));
This should do your thing.
Why not (I assume your type is person, not persons, you used both)
for (person p : persons)
{
if (!results.containsKey(p.getAge())
results.put(p.getAge(), new HashMap<income,persons>());
results.get(p.getAge()).put(p.getIncome(), p);
}
You can also implement your own collector for achieving this. With the help of Guava you can do it in one line:
Map<Integer, Map<Integer, Person>> result = persons.stream().collect(HashMap::new, (store, person) -> store.put(person.getAge(), ImmutableMap.of(person.getIncome(), person)), HashMap::putAll);
I second YaRiK; a Guava Table<Integer, Integer, Set<Person>> would work nicely here. You should use a Set<Person> to avoid collisions, like user902383 suggests. The streams API isn't always the right tool, and this looks to me like a case where a traditional iterative loop will be much easier to read.
Try this:
Table<Integer, Integer, Set<Person>> table = HashBasedTable.create();
for (Person p : persons) {
Set<Person> s = table.get(p.getAge(), p.getIncome());
if (s == null) {
s = new HashSet<>();
table.put(p.getAge(), p.getIncome(), s);
}
s.add(p);
}
What data structure should I use in the case described below:
I have a simple bean:
public class Points {
private String name;
private String address;
private int phone;
private int coord1;
private int coord2;
//getters+setters
}
I would like to create several beans and store them in some sort of data structure.
And be able to search with two parameters - name and address.
For example, user types in "7" - and it gives him back several object,
which name or address contains this character?
What data structure should i use and how do i search through it?
If it is important, I actually need this to implement into my android app -
i would like to search through my points on the map
Also I do not want to create a database so far, as there are only 20 of them.
Thank you very much in advance.
Try java's collection, e.g. hashmap. Although I ran this on PC, for 10000 items, with search
returned 3440 results, it took 76ms.
class Points {
String name;
String address;
int phone;
int coord1;
int coord2;
// getters+setters
};
class PointsIdentifier {
private String name;
private String address;
public PointsIdentifier(String name, String address) {
this.name = name;
this.address = address;
}
public boolean contains(String seq) {
return name.contains(seq) || address.contains(seq);
}
#Override
public boolean equals(Object obj) {
Points other = (Points) obj;
return name.equals(other.name) && address.equals(other.address);
}
#Override
public int hashCode() {
return name.hashCode() + address.hashCode();
}
};
class PointsCollection {
private Map<PointsIdentifier, Points> map;
public PointsCollection() {
map = new HashMap<PointsIdentifier, Points>();
}
public void add(Points p) {
map.put(new PointsIdentifier(p.name, p.address), p);
}
public List<Points> findIdsContaining(String seq) {
List<Points> resultList = new ArrayList<Points>();
for (Entry<PointsIdentifier, Points> entry : map.entrySet()) {
if (entry.getKey().contains(seq)) {
resultList.add(entry.getValue());
}
}
// optionally cache result
return resultList;
}
}
public class Question_11881630 {
public static void main(String[] args) {
PointsCollection places = createCollection(10000);
System.out.println("Collection created");
String seq = "1";
System.out.format("Searching for: \"%s\"\n", seq);
List<Points> verifySearch = verifySearch(places, seq);
//show(verifySearch);
}
private static void show(List<Points> verifySearch) {
int i = 1;
for (Points p : verifySearch) {
System.out.println(i + ": " + p.name + ", " + p.address);
i++;
}
}
private static List<Points> verifySearch(PointsCollection places, String seq) {
long start = System.currentTimeMillis();
List<Points> searchResult = places.findIdsContaining(seq);
System.out.println("Search results: " + searchResult.size());
long end = System.currentTimeMillis();
System.out.println("Operation time: " + formatTime(end - start));
return searchResult;
}
private static String formatTime(long elapsed) {
return elapsed + " miliseconds";
}
private static PointsCollection createCollection(int number) {
PointsCollection coll = new PointsCollection();
while (number > 0) {
coll.add(createSamplePoint(number));
number--;
}
return coll;
}
private static Points createSamplePoint(int number) {
Points p = new Points();
p.name = "VeryVeryLongName: " + number;
p.address = "VeryVeryLongLongAddress: " + number;
p.coord1 = 123;
p.coord2 = 456;
return p;
}
}
A trie seems a good fit. It is an efficient data structure to find all strings with a certain prefix.
If you want to use one of the existing java collections instead, you can use a TreeSet, and its floor() method to get the element before the needed prefix - and then start iterating the set while it still matches.
If you are looking for search by substring, and not only prefix - you might want to use a suffix tree instead.
An (inefficient) alternative that uses java's existing containers - is to store all substrings of your keys in a Set or a Map, but it will require quadric amount of space.