I have a simple list of strings. My goal is to get the last occurrences of each string in the list by group.
This is mode code:
List<String> newData = new ArrayList<>();
newData.add("A-something");
newData.add("A-fdfdsfds");
newData.add("A-fdsfdsfgs");
newData.add("B-something");
newData.add("B-dsafdrsafd");
newData.add("B-dsdfsad");
I wish to get only the last occurrence of each group. In other words I wanst to get "A-fdsfdsfgs" and "B-dsdfsad" only.
How to do so?
To get last occurrences for each group you can use stream api with groupingBy:
import static java.util.stream.Collectors.*;
Map<String, Optional<String>> collect = newData.stream()
.collect(groupingBy(strings -> strings.split("-")[0],
mapping(s -> s, maxBy(Comparator.comparingInt(newData::lastIndexOf)))));
Note: map has Optional as a value
To get it without Optional use toMap instead of groupingBy:
Map<String, String> collect = newData.stream()
.collect(toMap(s -> s.split("-")[0],
Function.identity(),
(s1, s2) -> newData.lastIndexOf(s1) > newData.lastIndexOf(s2) ? s1 : s2));
Also if you want to have map values without group name, then change Function.identity() with s -> s.split("-")[1]
import java.util.*;
class Solution {
public static void main(String[] args) {
List<String> newData = new ArrayList<>();
newData.add("A-something");
newData.add("A-fdfdsfds");
newData.add("A-fdsfdsfgs");
newData.add("B-something");
newData.add("B-dsafdrsafd");
newData.add("B-dsdfsad");
System.out.println(lastOccurrences(newData).toString());
}
private static List<String> lastOccurrences(List<String> data){
Set<String> set = new HashSet<>();
List<String> ans = new ArrayList<>();
for(int i=data.size()-1;i>=0;--i){
String group = data.get(i).substring(0,data.get(i).indexOf("-"));
if(set.contains(group)) continue;
set.add(group);
ans.add(data.get(i));
}
return ans;
}
}
Output:
[B-dsdfsad, A-fdsfdsfgs]
Algorithm:
Move from last to first, instead of first to last because you want last occurrences. This will make the management easier and code a little bit clean.
Get the group the string belongs to using substring() method.
Use a set to keep track of already visited groups.
If a group is not in the set, add it to the set and current string to our answer(since this will be the last occurred) for this group.
Finally, return the list.
There are several ways to this, as the other answers already show. I’d find something like the following natural:
Collection<String> lastOfEach = newData.stream()
.collect(Collectors.groupingBy((String s) -> s.split("-")[0],
Collectors.reducing("", s -> s, (l, r) -> r)))
.values();
lastOfEach.forEach(System.out::println);
With your list the output is:
A-fdsfdsfgs
B-dsdfsad
My grouping is the same as in a couple of other answers. On the grouped values I perform a reduction, each time I got two strings taking the latter of them. In the end this will give us the last string from each group as requested. Since groupingBy produces a map, I use values to discard the keys ( A and B) and get only the original strings.
Collecting via grouping should be sufficient.
final Map<String, List<String>> grouped =
newData.stream()
.collect(groupingBy(s -> s.split("-")[0]));
final List<String> lastOccurrences =
grouped.values()
.stream()
.filter(s -> !s.isEmpty())
.map(s -> s.get(s.size() - 1))
.collect(toList());
For Java 11, the filter becomes filter(not(List::isEmpty))
This will give you fdsfdsfgs, dsdfsad
Using a temporary Map. The List finalList will have only the required values
Map<String, String> tempMap = new HashMap<>();
List<String> finalList = new ArrayList<>();
newData.forEach((val) -> tempMap.put(val.split("-")[0], val.split("-")[1]));
tempMap.forEach((key, val) -> finalList.add(key + "-" + val));
Related
Let's say I have one list with elements like:
List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
And I have another large list of strings from which I would want to select all elements ending with any of the strings from the above list.
List<String> fullList= Arrays.asList("111.AAA", "222.AAA", "111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD");
Ideally I would want a way to partition the second list so that it contains four groups, each group containing only those elements ending with one of the strings from first list. So in the above case the results would be 4 groups of 2 elements each.
I found this example but I am still missing the part where I can filter by all endings which are contained in a different list.
Map<Boolean, List<String>> grouped = fullList.stream().collect(Collectors.partitioningBy((String e) -> !e.endsWith("AAA")));
UPDATE: MC Emperor's Answer does work, but it crashes on lists containing millions of strings, so doesn't work that well in practice.
Update
This one is similar to the approach from the original answer, but now fullList is no longer traversed many times. Instead, it is traversed once, and for each element, the list of endings is searched for a match. This is mapped to an Entry(ending, fullListItem), and then grouped by the list item. While grouping, the value elements are unwrapped to a List.
Map<String, List<String>> obj = fullList.stream()
.map(item -> endings.stream()
.filter(item::endsWith)
.findAny()
.map(ending -> new AbstractMap.SimpleEntry<>(ending, item))
.orElse(null))
.filter(Objects::nonNull)
.collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList())));
Original answer
You could use this:
Map<String, List<String>> obj = endings.stream()
.map(ending -> new AbstractMap.SimpleEntry<>(ending, fullList.stream()
.filter(str -> str.endsWith(ending))
.collect(Collectors.toList())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
It takes all endings and traverses the fullList for elements ending with the value.
Note that with this approach, for each element it traverses the full list. This is rather inefficient, and I think you are better off using another way to map the elements. For instance, if you know something about the structure of the elements in fullList, then you can group it immediately.
To partition a stream, means putting each element into one of two groups. Since you have more suffixes, you want grouping instead, i.e. use groupingBy instead of partitioningBy.
If you want to support an arbitrary endings list, you might prefer something better than a linear search.
One approach is using a sorted collection, using a suffix-based comparator.
The comparator can be implemented like
Comparator<String> backwards = (s1, s2) -> {
for(int p1 = s1.length(), p2 = s2.length(); p1 > 0 && p2 > 0;) {
int c = Integer.compare(s1.charAt(--p1), s2.charAt(--p2));
if(c != 0) return c;
}
return Integer.compare(s1.length(), s2.length());
};
The logic is similar to the natural order of string, with the only difference that it runs from the end to the beginning. In other words, it’s equivalent to Comparator.comparing(s -> new StringBuilder(s).reverse().toString()), but more efficient.
Then, given an input like
List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
List<String> fullList= Arrays.asList("111.AAA", "222.AAA",
"111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD");
you can perform the task as
// prepare collection with faster lookup
TreeSet<String> suffixes = new TreeSet<>(backwards);
suffixes.addAll(endings);
// use it for grouping
Map<String, List<String>> map = fullList.stream()
.collect(Collectors.groupingBy(suffixes::floor));
But if you are only interested in the count of each group, you should count right while grouping, avoiding to store lists of elements:
Map<String, Long> map = fullList.stream()
.collect(Collectors.groupingBy(suffixes::floor, Collectors.counting()));
If the list can contain strings which match no suffix of the list, you have to replace suffixes::floor with s -> { String g = suffixes.floor(s); return g!=null && s.endsWith(g)? g: "_None"; } or a similar function.
Use groupingBy.
Map<String, List<String>> grouped = fullList
.stream()
.collect(Collectors.groupingBy(s -> s.split("\\.")[1]));
s.split("\\.")[1] will take the yyy part of xxx.yyy.
EDIT : if you want to empty the values for which the ending is not in the list, you can filter them out:
grouped.keySet().forEach(key->{
if(!endings.contains(key)){
grouped.put(key, Collections.emptyList());
}
});
If your fullList have some elements which have suffixes that are not present in your endings you could try something like:
List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
List<String> fullList= Arrays.asList("111.AAA", "222.AAA", "111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD", "111.EEE");
Function<String,String> suffix = s -> endings.stream()
.filter(e -> s.endsWith(e))
.findFirst().orElse("UnknownSuffix");
Map<String,List<String>> grouped = fullList.stream()
.collect(Collectors.groupingBy(suffix));
System.out.println(grouped);
If you create a helper method getSuffix() that accepts a String and returns its suffix (for example getSuffix("111.AAA") will return "AAA"), you can filter the Strings having suffix contained in the other list and then group them:
Map<String,List<String>> grouped =
fullList.stream()
.filter(s -> endings.contains(getSuffix(s)))
.collect(Collectors.groupingBy(s -> getSuffix(s)));
For example, if the suffix always begins at index 4, you can have:
public static String getSuffix(String s) {
return s.substring(4);
}
and the above Stream pipeline will return the Map:
{AAA=[111.AAA, 222.AAA], CCC=[111.CCC, 222.CCC], BBB=[111.BBB, 222.BBB], DDD=[111.DDD, 222.DDD]}
P.S. note that the filter step would be more efficient if you change the endings List to a HashSet.
One can use groupingBy of substrings with filter to ensure that the final Map has just the Collection of relevant values. This could be sone as :
Map<String, List<String>> grouped = fullList.stream()
.collect(Collectors.groupingBy(a -> getSuffix(a)))
.entrySet().stream()
.filter(e -> endings.contains(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
private static String getSuffix(String a) {
return a.split(".")[1];
}
You can use groupingBy with filter on endings list as,
fullList.stream()
.collect(groupingBy(str -> endings.stream().filter(ele -> str.endsWith(ele)).findFirst().get()))
I have two lists of String objects. First list can have an object that can be splitted on basis of some delimiter. I want to get all those records of first list which are not present in second list.
List<String> list = Arrays.asList("A;H","B","C","D");
List collection = Arrays.asList("AH","B","D","E","F");
List<String> result = list.stream().filter(str -> !collection.contains(str)).collect(Collectors.toList());
The above code is partially correct according to my requirement. It outputs [A;H, C]. My requirement is it should only give output as C. Because if any of character or String object of "A;H" is present in second list. It should not be collected.
You can use a regular expression to eliminate the delimiting character and then compare the values for the existence. Here's how it looks.
private static final Pattern DELIMITER = Pattern.compile("\\W");
Set<String> set = new HashSet<>(collection);
List<String> nonExistingValues = list.stream()
.map(s -> DELIMITER.splitAsStream(s).collect(Collectors.joining()))
.filter(s -> !set.contains(s))
.collect(Collectors.toList());
A much more compact approach which reuses the pattern as mentioned in the below comment would be,
List<String> nonExistingValues = list.stream()
.map(s -> DELIMITER.matcher(s).replaceAll(""))
.filter(s -> !set.contains(s))
.collect(Collectors.toList());
I have an int arry input, for example : [1,3,4].
I also have a fixed/constant map of :
1 -> A
2 -> B
3 -> C
4 -> D
5 -> E
I want my output to be a corresponding string of all the relevant keys.
For our example of input [1,3,4], the output should be : "A,C,D".
What's the most efficient way of achieving that?
My idea was to iterate over the whole map, each time.
The problem with that, is that I have a remote call in android that fetches a long list of data items, and doing that for each item in the list seems a bit.. inefficient. Maybe there's something more efficient and/or more elegant. Perhaps using Patterns
Assuming the array is defined as below along with the HashMap:
int arr[] = { 1, 3, 4 };
HashMap<Integer, String> hmap = new HashMap<>();
// data in the map
hmap.put(1, "A"); hmap.put(2, "B"); hmap.put(3, "C"); hmap.put(4, "D"); hmap.put(5, "E");
Instead of iterating over the entire map, you can iterate over the array
String[] array = Arrays.stream(arr).mapToObj(i -> hmap.get(i))
.filter(Objects::nonNull)
.toArray(String[]::new);
This gives the output :
A C D
As per your comment, to join it as one String you can use :
String str = Arrays.stream(arr).mapToObj(i -> hmap.get(i))
.filter(Objects::nonNull)
.collect(Collectors.joining("/"));
You can iterate over the list of input instead of a map. That's the benefit of using a Map by the way, it has a lookup time of O(1) with proper hashing implemented. It would somewhat like :
Map<Integer, String> data = new HashMap<>();
List<Integer> query = new ArrayList<>(); // your query such as "1,3,4"
List<String> information = new ArrayList<>();
for (Integer integer : query) {
String s = data.get(integer); // looking up here
information.add(s);
}
which with the help of java-stream can be changed to
List<String> information = query.stream()
.map(data::get) // looking up here
.collect(Collectors.toList()); // returns "A,C,D"
Note: I have used String and Integer for just a representation, you can use your actual data types there instead.
I have list of ImmutableTriple object where for first and middle there could collection of last values (first, middle and last are triple values).
Now in order to make it queryable, I need to convert it to Guava Table data structure. I am able to achieve this, with for loop as below but I am wondering if I can achieve this more functionally using lambda expression.
here is the piece code -
public static void main(String[] args) {
//In real world, this list is coming from various transformation of lamda
final List<ImmutableTriple<LocalDate, Integer, String>> list = ImmutableList.of(
ImmutableTriple.of(LocalDate.now(), 1, "something"),
ImmutableTriple.of(LocalDate.now(), 1, "anotherThing")
);
Table<LocalDate, Integer, List<String>> table = HashBasedTable.create();
//is it possible to avoid this forEach and use side effect free lambda.
list.forEach(s -> {
final List<String> strings = table.get(s.left, s.middle);
final List<String> slotList = strings == null ? new ArrayList<>() : strings;
slotList.add(s.right);
table.put(s.left, s.middle, slotList);
});
System.out.println(table);
}
There is a Tables class which contains a Collector to get your desired result.
Table<LocalDate, Integer, ImmutableList<String>> collect = list.stream()
.collect(Tables.toTable(
it -> it.left,
it -> it.middle,
it -> ImmutableList.of(it.right),
(l1, l2) -> ImmutableList.<String>builder()
.addAll(l1).addAll(l2).build(),
HashBasedTable::create));
If you really want a mutable List then you can use:
Table<LocalDate, Integer, List<String>> collect = list.stream()
.collect(Tables.toTable(
it -> it.left,
it -> it.middle,
it -> Lists.newArrayList(it.right),
(l1, l2) -> {l1.addAll(l2); return l1;},
HashBasedTable::create));
I want to collect the items in a stream into a map which groups equal objects together, and maps to the number of occurrences.
List<String> list = Arrays.asList("Hello", "Hello", "World");
Map<String, Long> wordToFrequency = // what goes here?
So in this case, I would like the map to consist of these entries:
Hello -> 2
World -> 1
How can I do that?
I think you're just looking for the overload which takes another Collector to specify what to do with each group... and then Collectors.counting() to do the counting:
import java.util.*;
import java.util.stream.*;
class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Hello");
list.add("Hello");
list.add("World");
Map<String, Long> counted = list.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(counted);
}
}
Result:
{Hello=2, World=1}
(There's also the possibility of using groupingByConcurrent for more efficiency. Something to bear in mind for your real code, if it would be safe in your context.)
Here is example for list of Objects
Map<String, Long> requirementCountMap = requirements.stream().collect(Collectors.groupingBy(Requirement::getRequirementType, Collectors.counting()));
Here are slightly different options to accomplish the task at hand.
using toMap:
list.stream()
.collect(Collectors.toMap(Function.identity(), e -> 1, Math::addExact));
using Map::merge:
Map<String, Integer> accumulator = new HashMap<>();
list.forEach(s -> accumulator.merge(s, 1, Math::addExact));
List<String> list = new ArrayList<>();
list.add("Hello");
list.add("Hello");
list.add("World");
Map<String, List<String>> collect = list.stream()
.collect(Collectors.groupingBy(o -> o));
collect.entrySet()
.forEach(e -> System.out.println(e.getKey() + " - " + e.getValue().size()));
Here is the simple solution by StreamEx:
StreamEx.of(list).groupingBy(Function.identity(), MoreCollectors.countingInt());
This has the advantage of reducing the Java stream boilerplate code: collect(Collectors.
If you're open to using a third-party library, you can use the Collectors2 class in Eclipse Collections to convert the List to a Bag using a Stream. A Bag is a data structure that is built for counting.
Bag<String> counted =
list.stream().collect(Collectors2.countBy(each -> each));
Assert.assertEquals(1, counted.occurrencesOf("World"));
Assert.assertEquals(2, counted.occurrencesOf("Hello"));
System.out.println(counted.toStringOfItemToCount());
Output:
{World=1, Hello=2}
In this particular case, you can simply collect the List directly into a Bag.
Bag<String> counted =
list.stream().collect(Collectors2.toBag());
You can also create the Bag without using a Stream by adapting the List with the Eclipse Collections protocols.
Bag<String> counted = Lists.adapt(list).countBy(each -> each);
or in this particular case:
Bag<String> counted = Lists.adapt(list).toBag();
You could also just create the Bag directly.
Bag<String> counted = Bags.mutable.with("Hello", "Hello", "World");
A Bag<String> is like a Map<String, Integer> in that it internally keeps track of keys and their counts. But, if you ask a Map for a key it doesn't contain, it will return null. If you ask a Bag for a key it doesn't contain using occurrencesOf, it will return 0.
Note: I am a committer for Eclipse Collections.