Pluck only relevent keys from a map - java

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.

Related

Matching 2 ArrayLists of objects based on their member variables

Let's say I have 2 objects, Object1 and Object2. Their basic structure is as follows:
Object 1
int id
String email
Object 2
int id
ArrayList<String> emails
Now I have 2 ArrayLists, one of Object1 and Object2. What's an efficient way to find matches where Object 1's email is contained inside Object 2's emails ArrayList and then store their ids in a HashMap (or any other data structure that holds 2 ints)?
I know the obvious and basic solution is to brute force it with 2 for loops, like this:
ArrayList<Object1> obj1List;
ArrayList<Object2> obj2List;
HashMap<Integer, Integer> idMapping = new HashMap()<>;
for (Object1 obj1 : obj1List){
String obj1Email = obj1.getEmail();
for (Object2 obj2 : obj2List){
ArrayList<String> obj2EmailList = obj2.getEmails();
if(obj2EmailList.contains(obj1Email)){
int obj1Id = obj1.getId();
int obj2Id = obj2.getId();
idMapping.put(obj1Id, obj2Id);
}
}
}
Each ArrayList has around a thousand objects, so performance really isn't that big of an issue. However, I'm sure there's much more elegant ways of solving this problems. I'm guessing it might be possible using streams, but I'm not familiar enough with them to do it. Any suggestions?
I think the way you are doing is absolutely fine. However, if you want to use streams, you can try this approach.
Since you are going to traverse across the List of Object2 pretty frequently for every Object1, I would recommend creating a Map for Object2 List so that your retrieval is faster.
Based on your code, I think I can map each email to it's individual id assuming an email in Object2 List can never have more than 1 id which I think is true based on your implementation.
If my assumption is wrong, there is nothing much we can do.
Here is the code:
Map<String, Integer> obj2Map = new HashMap<>();
for (Object2 obj2 : obj2List) {
int id = obj2.getId();
obj2Map.putAll(
obj2.getEmails()
.stream()
.collect(Collectors.toMap(String::toString, email -> id))
);
}
Map<Integer, Integer> idMapping = new HashMap();
for (Object1 obj1 : obj1List) {
if (obj2Map.containsKey(obj1.getEmail())) {
idMapping.put(obj1.getId(), obj2Map.get(obj1.getEmail()));
}
}
Map<Integer, Integer> collect = object1List.stream()
.flatMap(ob1 -> object2List.stream().filter(ob2 -> ob2.getEmails().contains(ob1.getEmail())).map(ob2 -> {
int [] arr = new int[2];
arr[0] = ob1.getId();
arr[1] = ob2.getId();
return arr;
}))
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
you could use flatMap to flatten the list of emails in the second Object and compare to the first object using ArrayList contains method.
the above code is the stream version of your code however there could possibly be duplicate keys in the map.
As idMapping I would recommend use Map<Integer, List<Integer>>. There could be more ids in obj2List witch the same email. For example with this data:
List<Object1> obj1List = Arrays.asList(
new Object1(1, "a"),
new Object1(2, "b"),
new Object1(3, "c"));
List<Object2> obj2List = Arrays.asList(
new Object2(11, "a"),
new Object2(12, "a", "b"),
new Object2(14, "c", "d"),
new Object2(15, "e", "f")
);
When HashMap<Integer, Integer> idMapping you get:
{1=12, 2=12, 3=14}
But for Map<Integer, List<Integer>> idMapping it will be:
{1=[11, 12], 2=[12], 3=[14]}
The main solution could be like below. Create map (grouping by email) and then use it to create idMapping :
Map<String, List<Integer>> idsForEmailFromObj2List = obj2List.stream().
flatMap(obj2 -> obj2.getEmails().stream().map(
email -> new Object1(obj2.getId(), email)
))
.collect(Collectors.groupingBy(
Object1::getEmail,
Collectors.mapping(Object1::getId, Collectors.toList())));
Map<Integer, List<Integer>> idMapping = obj1List.stream()
.filter(obj1 -> idsForEmailFromObj2List.containsKey(obj1.getEmail()))
.collect(Collectors.toMap(
Object1::getId,
obj1 -> idsForEmailFromObj2List.get(obj1.getEmail())));

List<String> get count of all elements ending with one of strings from another list

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

Getting last occurrences of specific string in a list

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

Loop through n number of maps

Right now I have the following code, which takes 2 recipes and finds duplicates in the recipes and "merges" them.
public void mergeIngredients(Recipe recipe1, Recipe recipe2) {
Map<String, Ingredients> recipe1Map = recipe1.getIngredientsMap();
Map<String, Ingredients> recipe2Map = recipe2.getIngredientsMap();
for (Map.Entry<String, Ingredients> s : recipe1Map.entrySet()) {
if (recipe2Map.containsKey(s.getKey())) {
double newValue = recipe1.getAmount(s.getKey()) + recipe2.getAmount(s.getKey());
System.out.println(newValue);
}
}
}
I want to change this code so instead of only being able to check 2 maps against each other, I need to refactor the code so it can take N number of maps and compare all of them.
Example: The user inputs 8 different recipes, it should loop through all of these and merge ingredients if duplicates are found. What is the best way to achieve this?
I would first extract all keys from all Maps into a Set. This gives you all unique ingredients-keys.
Then iterate that Set and get all the values from all the recipes and merge them.
For example:
public void mergeIngredients(Set<Recipe> recipes) {
Set<String> keys = recipes.stream() //
.map(Recipe::getIngredientsMap) // Get the map
.flatMap(m -> m.keySet().stream()) // Get all keys and make 1 big stream
.collect(Collectors.toSet()); // Collect them to a set
for (String k : keys)
{
double newValue = recipes.stream() //
.map(Recipe::getIngredientsMap) //
.map(i->i.get(k)) //
.mapToDouble(i->i.getAmount()) //
.sum(); //
System.out.println(newValue);
}
}
You problably can do this more efficient; but this is easier to follow I think.
You can use Merging Multiple Maps Using Java 8 Streams in the case of duplicate keys:
public void mergerMap() throws Exception {
Map<String, Integer> m1 = ImmutableMap.of("a", 2, "b", 3);
Map<String, Integer> m2 = ImmutableMap.of("a", 3, "c", 4);
Map<String, Integer> mx = Stream.of(m1, m2)
.map(Map::entrySet) // converts each map into an entry set
.flatMap(Collection::stream) // converts each set into an entry stream, then
// "concatenates" it in place of the original set
.collect(
Collectors.toMap( // collects into a map
Map.Entry::getKey, // where each entry is based
Map.Entry::getValue, // on the entries in the stream
Integer::max // such that if a value already exist for
// a given key, the max of the old
// and new value is taken
)
)
;
Map<String, Integer> expected = ImmutableMap.of("a", 3, "b", 3, "c", 4);
assertEquals(expected, mx);
}
I don't really see the need of a Map for your ingredients so here is an alternative solution.
If you make your Ingredients class implement equals & hashcode you can use it directly in a Set. You will of course also have a method in Recipe that returns all ingredients as a List. Then the following will return all unique ingredients.
Set<Ingredients> merge(List<Recipe> recipies) {
return recipies.stream().map(s -> s.allIngredients()).collect(Collectors.toSet());
}

Split 1 ArrayList into multiple ones based on condition

I have a big ArrayList containing strings. I want to split it, based on a condition that an element meets. For example, it could be the string length if ArrayList contained String. What is the most efficient (not the easiest) way to do that ?
['a', 'bc', 'defe', 'dsa', 'bb']
after would result to :
['a'], ['bc', 'bb'], ['dsa'], ['defe']
It's easy and fairly efficient to do it using Java 8 streams:
Collection<List<String>> output = input.stream()
.collect(Collectors.groupingBy(String::length))
.values();
If you run it with this input:
List<String> input = Arrays.asList("a", "bc", "defe", "dsa", "bb");
You will get this output:
[[a], [bc, bb], [dsa], [defe]]
A non-stream version would do the same thing, i.e. build a Map<K, List<V>> where V is your value type (e.g. String in your case), and K is the type of the grouping value (e.g. Integer for the length).
Doing it yourself (like shown in answer by palako) might be slightly more efficient at runtime, but likely not in any way that matters.
Staying with Java 8, that would be this:
Map<Integer, List<String>> map = new HashMap<>();
for (String value : input)
map.computeIfAbsent(value.length(), ArrayList::new).add(value);
Collection<List<String>> output = map.values();
For earlier versions of Java, you can't use computeIfAbsent(), so:
Map<Integer, List<String>> map = new HashMap<Integer, List<String>>();
for (String value : input) {
Integer length = Integer.valueOf(value.length()); // box only once
List<String> list = map.get(length);
if (list == null)
map.put(length, list = new ArrayList<String>());
list.add(value);
}
Collection<List<String>> output = map.values();
The most efficient way is to iterate the original list just once. What you do is you create buckets and add to those buckets.
public class Q1 {
public static void main(String[] args) {
String[] original = {"a","bc","defe","dsa","bb"};
List<String> originalValues = new ArrayList<String>(Arrays.asList(original));
Map<Integer, List<String>> orderedValues = new HashMap<Integer, List<String>>();
Iterator<String> it = originalValues.iterator();
while (it.hasNext()) {
String currentElement = it.next();
int length = currentElement.length();
if(!orderedValues.containsKey(length)) {
orderedValues.put(length, new ArrayList<String>());
}
orderedValues.get(length).add(currentElement);
}
System.out.println(orderedValues.values());
}
}
You might be tempted to use an array of arrays instead of a Map, and use the size of the string as the index to the array position, but then you need to watch out for a case where you don't have strings of a certain length. Imagine a case where you only have a string in the original list, but it has 100 characters. You would have 99 empty positions and one string in the array at position 100.

Categories

Resources