I have a List of Teams with the following properties:
public class Team {
private String name; //get;set
private int score;//get;set
}
Is there a way to group the results of the list by Name while adding the scores?
The List can contain multiple of the same team but with different scores:
TeamA - 3
TeamB - 3
TeamA - 3
TeamC - 0
TeamB - 1
TeamD - 1
TeamE - 1
And the final list that I want to achive is something like this (ordered by score and when draw, alphabetically):
TeamA - 6
TeamB - 4
TeamD - 1
TeamE - 1
TeamC - 0
Right now, I got this with the help of frascu's answer:
teams.sort(Comparator.comparing(Team::getName)
.thenComparing(Team::getScore, Comparator.reverseOrder()));
Map<String, Integer> map = teams.stream()
.collect(groupingBy(Team::getName, summingInt(Team::getScore)));
But when I print the result from the map I get this:
TeamA - 6
TeamB - 4
TeamC - 0
TeamD - 1
TeamE - 1
The score 0 is being ordered as higher than 1
It's not necessary to sort the source list unless you don't want it to be sorted as well.
Note that by sorting the source list you can not ensure that entries of the map would be stored in sorted order because the map is expected to contain the accumulated score (i.e. multiple elements in the list could contribute to a single map entry). Hence, sorting has to be applied right in the stream after grouping the data by team-name and generating the total score of each team.
In order to maintain entries in sorted order, you need a Map implementation which is capable of maintaining the order. When mapFactory is not specified, collector would give you a general purpose implementation (for now it's HashMap) which doesn't meet this requirement.
The possible option is to use a LinkedHashMap. Note: because entries need to be ordered by both value and key, you can't achieve required ordering with a TreeMap, which maintains sorted order based on keys.
That how it might be implemented:
List<Team> teams = List.of(
new Team("TeamA", 3),
new Team("TeamB", 3),
new Team("TeamA", 3),
new Team("TeamC", 0),
new Team("TeamB", 1),
new Team("TeamD", 1),
new Team("TeamE", 1)
);
Map<String, Integer> scoreByName = teams.stream()
.collect(Collectors.toMap(
Team::getName, // keyMapper
Team::getScore, // valueMapper
Integer::sum // mergeFunction
))
.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(left, right) -> { throw new AssertionError("duplicates are not expected"); }, // because the source of the stream is a Map, and there couldn't be a key that occure more than once
LinkedHashMap::new // mapFactory
));
scoreByName.forEach((k, v) -> System.out.println(k + " -> " + v));
Output:
TeamA -> 6
TeamB -> 4
TeamD -> 1
TeamE -> 1
TeamC -> 0
A link to Online Demo
Using thenComparing you can apply a second sort on the elements that have the same name. In your case, you need to call reverseOrder in order to apply descending order to the score.
list.sort(Comparator.comparing(Team::getName)
.thenComparing(Team::getScore, Comparator.reverseOrder()));
Related
How to using java stream, check if list of integers contains two groups of different repeated numbers. Number must be repeated not more then two time.
Example: list of 23243.
Answer: true, because 2233
Example 2: list of 23245.
Answer: none
Example 3: list of 23232.
Answer: none, because 222 repeated three times
One more question, how can i return not anyMatch, but the biggest of repeated number?
listOfNumbers.stream().anyMatch(e -> Collections.frequency(listOfNumbers, e) == 2)
This will tell you if the list meets your requirements.
stream the list of digits.
do a frequency count.
stream the resultant counts
filter out those not equal to a count of 2.
and count how many of those there are.
Returns true if final count == 2, false otherwise.
List<Integer> list = List.of(2,2,3,3,3,4,4);
boolean result = list.stream()
.collect(Collectors.groupingBy(a -> a,
Collectors.counting()))
.values().stream().filter(count -> count == 2).limit(2)
.count() >= 2; // fixed per OP's comment
The above prints true since there are two groups of just two digits, namely 2's and 4's
EDIT
First, I made Holger's suggestion to short circuit the count check.
To address your question about returning multiple values, I broke up the process into parts. The first is the normal frequency count that I did before. The next is gathering the information requested. I used a record to return the information. A class would also work. The max count for some particular number is housed in an AbstractMap.SimpleEntry
List<Integer> list = List.of(2, 3, 3, 3, 4, 4, 3, 2, 3);
Results results = groupCheck(list);
System.out.println(results.check);
System.out.println(results.maxEntry);
Prints (getKey() and getValue() may be used to get the individual values. First is the number, second is the occurrences of that number.)
true
3=5
The method and record declaration
record Results(boolean check,
AbstractMap.SimpleEntry<Integer, Long> maxEntry) {
}
Once the frequency count is computed, simply iterate over the entries and
count the pairs and compute the maxEntry by comparing the existing maximum count to the iterated one and update as required.
public static Results groupCheck(List<Integer> list) {
Map<Integer, Long> map = list.stream().collect(
Collectors.groupingBy(a -> a, Collectors.counting()));
AbstractMap.SimpleEntry<Integer, Long> maxEntry =
new AbstractMap.SimpleEntry<>(0, 0L);
int count = 0;
for (Entry<Integer, Long> e : map.entrySet()) {
if (e.getValue() == 2) {
count++;
}
maxEntry = e.getValue() > maxEntry.getValue() ?
new AbstractMap.SimpleEntry<>(e) : maxEntry;
}
return new Results(count >= 2, maxEntry);
}
One could write a method which builds a TreeMap of the frequencies.
What happens here, is that a frequency map is built first (by groupingBy(Function.identity(), Collectors.counting()))), and then we must 'swap' the keys and values, because we want to use the frequencies as keys.
public static TreeMap<Long, List<Integer>> frequencies(List<Integer> list) {
return list.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.collect(Collectors.toMap(e -> e.getValue(), e -> List.of(e.getKey()), (a, b) -> someMergeListsFunction(a, b), TreeMap::new));
}
And then we can just use our method like this:
// We assume the input list is not empty
TreeMap<Long, List<Integer>> frequencies = frequencies(list);
var higher = frequencies.higherEntry(2L);
if (higher != null) {
System.out.printf("There is a number which occurs more than twice: %s (occurs %s times)\n", higher.getValue().get(0), higher.getKey());
}
else {
List<Integer> occurTwice = frequencies.lastEntry().getValue();
if (occurTwice.size() < 2) {
System.out.println("Only " + occurTwice.get(0) " occurs twice...");
}
else {
System.out.println(occurTwice);
}
}
A TreeMap is a Map with keys sorted by some comparator, or the natural order if none is given. The TreeMap class contains methods to search for certain keys. For example, the higherEntry method returns the first entry which is higher than the given key. With this method, you can easily check if a key higher than 2 exists, for one of the requirements is that none of the numbers may occur more than twice.
The above code checks whether there is a number occurring more than twice, that is when higherEntry(2L) returns a nonnull value. Otherwise, lastEntry() is the highest number occurring. With getValue(), you can retrieve the list of these numbers.
I have a list of items. Each item has 3 properties: name, section and amount. The amount can be positive/negative. The same item can be several times in the same section.
I want to group the items in the list by section and name. If the sum of this grouping equals 0 - remove all the items with this section and name.
I got this far
items.stream().collect(Collectors.groupingBy(ItemData::getSection))....
Now I need the second grouping and compare it to 0 in order to add it as a predicate to removeIf
items.removeIf(item -> predicate(item));
You can first group the ItemData based on section and name with sum of amount as value
Map<String,Long> result = details.stream()
.collect(Collectors.groupingBy(det -> det.getSection()+"_"+det.getName(),
Collectors.summingLong(det->det.getAmount())));
And then remove the entries from Map having value !=0
result.entrySet().removeIf(det->det.getValue() != 0);
And then remove the elements from items list having entry in Map with key comparison
items.removeIf(item -> result.containsKey(item.getSection()+"_"+item.getName()));
You can also eliminate the entries having value 0 by using stream
Map<String,Long> result = details.stream()
.collect(Collectors.groupingBy(det -> det.getSection()+"_"+det.getName(),
Collectors.summingLong(det->det.getCount())))
.entrySet()
.stream()
.filter(entry->entry.getValue()==0)
.collect(Collectors.toMap(Map.Entry::getKey,Map.Entry::getValue));
Alternatively you could use a BiPredicate which accepts an Item and List<Item>
BiPredicate<Item,List<Item>> biPred =
(item, list) -> list.stream()
.filter(i -> i.getName().equals(item.getName()) &&
i.getSection().equals(item.getSection()))
.mapToLong(Item::getAmount)
.sum() == 0;
myList.removeIf(e -> biPred.test(e, myList));
Map is defined as:
Map<Integer,String> map = new HashMap<>();
map.put(2,"ram");
map.put(3,"ram");
map.put(4,"gopal");
map.put(5,"madan");
map.put(6,"shyam");
map.put(7,"gopal");
map.put(8,"ram");
My expected output is List which contains only keys for which there is no duplicate values.
5
6
My approach and thought process:
Thought process 1 :
I would take map.entrySet().stream().map(....) and then take another stream inside map and filter the values for which duplicate values are present.
The approach soon got wasted as the first indexed value would be again compared in the nested stream and i would happen to filter out all elements thus.
Thought process 2
I kept values in different List by :
List<String> subList = map.entrySet().stream()
.map((k)->k.getValue())
.collect(Collectors.toList());
and then:
map.entrySet().stream()
.filter(s ->
subList.contains(s.getValue()) )
.map(Map.Entry::getKey)
.collect(Collectors.toList());
But I am getting the output as
2
3
4
5
6
7
8
The output is obvious as the value that i am picking as s from stream i am comparing it in the pool where the value will be always present at-least one time.
I again thought then that if i could have a counter that count count and if the value is present then it would increment, but again all seems very much vague now.
Any ways in which i can iterate by index using stream, so that i can always leave the key value which i am taking and just comparing with rest of values.
will love to get a brief explanation.
You can break the task into two step. first count value repeating by groupingBy() and counting() collectors.
Map<String,Long> valueCount = map.values()
.stream()
.collect(Collectors.groupingBy(Function.identity(),Collectors.counting()));
Its result is:
{madan=1, shyam=1, gopal=2, ram=3}
second step is to find only keys which their values are not duplicate. so to attain this you can use filter() and filtering the map by previous step result.
map.entrySet()
.stream()
.filter(entry -> valueCount.get(entry.getValue())==1).map(Map.Entry::getKey)
.collect(Collectors.toList())
You can filter the values with frequency 1 while creating the subList such as:
Set<String> uniqueSet = map.values().stream()
.collect(Collectors.groupingBy(a -> a, Collectors.counting()))
.entrySet().stream()
.filter(a -> a.getValue() == 1)
.map((Map.Entry::getKey))
.collect(Collectors.toSet());
and then perform the same operation as:
Set<Integer> result = map.entrySet().stream()
.filter(e -> uniqueSet.contains(e.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
Or as Holger pointed out in the comments, instead of counting, you could do away with a Boolean value to filter unique values as:
Set<String> uniqueSet = map.values().stream()
.collect(Collectors.toMap(Function.identity(), v -> true, (a,b) -> false))
.entrySet().stream()
.filter(Map.Entry::getValue)
.map((Map.Entry::getKey))
.collect(Collectors.toSet());
Let`s assume, that we have List of samples (type Double) {1.5, 1.1, 2.2, 1.0, 2.2, 3.3}. How can I achieve a Map that holds Integer as key (groups) and number of occurrences as a Value of this Map?
For given example 1 -> 3, 2 -> 2, 3 -> 1. I know I can achieve this with if/else if or case logic but in my app it would be 30 different groups (30 cases or elseifs in code).
App is creating wind histogram from given wind speed pulled from DB (millions of samples).
My approach:
Map<Double, Long> map = windData.stream()
.collect(Collectors.groupingBy(WindData::getSpeed, Collectors.counting()));
Where windData is list holding wind speeds, and getSpeed is retrieving wind speed value. This approach generates groups of doubles, which means that it only count occurrences and I would like to get 0-1, 1-2, 2-3, 3-4 etc. groups.
Just round the double down to an int in the first parameter to groupingBy. Your question first says that List of samples contains Doubles but your code snippet suggests that it is actually a List<WindData>. Which is it?
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
....
List<Double> speeds = ...;
Map<Integer, Long> histogram = speeds.stream()
.collect(groupingBy(Double::intValue, counting()));
On a separate note, unless you are pulling this data from the database anyway for some other reason, consider doing this on the database side with SQL group by
This is a pipeline producing "histogram" data from a stream of double values:
HashMap<Double, Long> res =
DoubleStream.of(1.5, 1.1, 2.2, 1.0, 2.2, 3.3)
.collect(HashMap<Double, Long>::new,
(map, dbl) -> map.merge(Math.floor(dbl), 1L, (k, v) -> k + v),
(map1, map2) -> map1.putAll(map2));
Printing res outputs {2.0=2, 1.0=3, 3.0=1}.
To apply this to your object stream, I'd just convert it to a double stream using mapToDouble:
windData.stream().mapToDouble(WindData::getSpeed)
.collect(HashMap<Double, Long>::new,
(map, dbl) -> map.merge(Math.floor(dbl), 1L, (k, v) -> k + v),
(map1, map2) -> map1.putAll(map2));
Note about the result: The keys of the map are the lower boundary of the interval (0 is 0-1 and 1 is 1-2)
I am writing a method which takes an input Map of the form Map<Term, List<Integer>> where a Term is defined here.
Method:
Go over the keys of the Map and filter them using a Term attribute.
For each of the remaining keys, get the size of the corresponding list, cap it to 5 (min(List.size(), 5)) and add the output to a global var (say, totalSum)
Return totalSum
This is what I have written so far:
inputMap
.entrySet()
.stream()
.filter(entry -> entry.getKey().field().equals(fieldName)) // Keep only terms with fieldName
.forEach(entry -> entry.getValue()
.map(size -> Math.min(entry.getValue().size(), 5))) // These 2 lines do not work
.sum();
I am unable to take as input a stream of lists, output an integer for each of those lists and return the sum of all the outputs.
I can obviously write it using for loops, but I am trying to learn Java 8 and was curious if this problem is solvable using it.
You don't need the forEach method. You can map each entry of the Map to an int, and sum those integers :
int sum = inputMap
.entrySet()
.stream()
.filter(entry -> entry.getKey().field().equals(fieldName))
.mapToInt(entry -> Math.min(entry.getValue().size(), 5))
.sum();
With Eclipse Collections, the following will work using MutableMap and IntList.
MutableMap<Term, IntList> inputMap =
Maps.mutable.of(term1, IntLists.mutable.of(1, 2, 3),
term2, IntLists.mutable.of(4, 5, 6, 7));
long sum = inputMap
.select((term, intList) -> term.field().equals(fieldName))
.sumOfInt(intList -> Math.min(intList.size(), 5));
Note: I am a committer for Eclipse Collections.
The forEach invocation terminates the stream. You can use map directly without forEach.