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());
}
Related
How to sort HashMap entries by Value and print all the Keys mapped the Lowest Value
Here is my HashMap.
HashMap<String, Integer> map = new HashMap<>();
map.put("John", 1);
map.put("Matthew", 12);
map.put("Clara", 53);
map.put("Keith", 2);
Expected output:
John
I am trying to sort them, so I can get "John" to be collected in the list.
List<String> keys = res.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue())
.limit(1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
Now, let's consider another map.
HashMap<String, Integer> map = new HashMap<>();
map.put("John", 2);
map.put("Matthew", 12);
map.put("Clara", 53);
map.put("Keith", 2);
Expected output:
John, Keith
How can I get both John and Keith to be added to the list in case of a tie (i.e. both associated with the lowest value) ?
Collect the entries into a Map<Integer, List<Map.Entry>> grouping by their values, get the entries that correspond to the minimum value, then convert those entries to a list of their keys:
List<String> lowestValuedNames = map.entrySet().stream()
.collect(Collectors.groupingBy(Map.Entry::getValue))
.get(Collections.min(map.values()))
.stream()
.map(Map.Entry::getKey)
.collect(Collectors.toList());
See live demo.
Not particularly efficient, but done using one line.
It can be done without sorting, as well as without creating an intermediate map (and performing an additional iteration over its entries to find the entry with the lowest key). I.e. we can do better in terms of both memory allocation and performance.
For that, we need to maintain a collection of entries having the lowest value encountered so far and compare the value of every encountered entry with the value of the first element in the collection. If the collection is empty or if values are the same, we need to add the next entry. If the value of the next entry is lower, we need to clear the collection and then add the entry. And the value of the next entry is greater than we should ignore it.
To implement this approach with streams, we can create a custom collector using static method Collector.of().
That's how it can be implemented:
public static void main(String[] args) {
Map<String, Integer> people =
Map.of("John", 2, "Matthew", 12,
"Clara", 53, "Keith", 2);
List<String> result = people.entrySet().stream()
.collect(Collector.of(
ArrayDeque::new,
(Deque<Map.Entry<String, Integer>> deque, Map.Entry<String, Integer> next) -> {
if (!deque.isEmpty() && deque.peek().getValue() > next.getValue()) deque.clear();
if (deque.isEmpty() || deque.peek().getValue().equals(next.getValue())) deque.add(next);
},
(left, right) -> {
if (left.isEmpty()) return right;
if (right.isEmpty()) return left;
if (left.peek().getValue() < right.peek().getValue()) return left;
if (left.peek().getValue() > right.peek().getValue()) return right;
left.addAll(right);
return left;
},
deque -> deque.stream().map(Map.Entry::getKey).collect(Collectors.toList())
));
System.out.println(result);
}
The collector is still performing the final iteration over the resulting collection of entries to extract the keys. We can omit this step and simplify the code by encapsulating the logic that currently resides in the collector into a separate class. And the instance of that class would be used by the collector as a mutable container instead of a collection and will be responsible for doing all the housekeeping.
class EntryContainer implements Consumer<Map.Entry<String, Integer>> {
private int value;
private List<String> names = new ArrayList<>();
#Override
public void accept(Map.Entry<String, Integer> next) {
if (!names.isEmpty() && next.getValue() < value) names.clear();
if (names.isEmpty() || value == next.getValue()) {
names.add(next.getKey());
value = next.getValue();
}
}
public EntryContainer merge(EntryContainer other) {
if (names.isEmpty() || !other.names.isEmpty() && other.value < value) return other;
if (other.names.isEmpty() || value < other.value) return this;
names.addAll(other.names);
return this;
}
public List<String> getNames() {
return names;
}
}
Now we can apply it. The collector no longer looks intimidating, and we don't need a finisher function, instead the collection of names is being handed out by the collector's container at the end of the stream execution:
public static void main(String[] args) {
Map<String, Integer> people =
Map.of("John", 2, "Matthew", 12,
"Clara", 53, "Keith", 2);
List<String> result = people.entrySet().stream()
.collect(Collector.of(
EntryContainer::new,
EntryContainer::accept,
EntryContainer::merge
))
.getNames();
System.out.println(result);
}
Output:
[Keith, John]
I need to map a list of pairs of objects into <ocurrences, list of Objs with those ocurrences>, I've tried using streams directly on the input list of pairs but I'm still kind of new to java and couldn't figure it out, so I was trying to do something like this, but it's probably not close to the best way to do it.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
Map<Integer,ArrayList<Obj>> nBorders = new HashMap<>();
List<Obj> list = new ArrayList<>();
for(Pair<Obj, Obj> pair : lf) {
list.add(pair.getKey());
list.add(pair.getValue());
}
nBorders = list.stream().collect(Collectors.groupingBy(...);
return nBorders;
}
so for example, for lf = {(o1,o2),(o3,o2),(o5,o4),(o4,o1),(o3,o4),(o7,o1),(o5,o8),(o3,o10),(o4,o5),(o3,o7),(o9,o8)} the result should be {(1,{o9,o10}),(2,{o2,o7,o8,}),(3,{o1,o5}),(4,{o3,o4})}.
I'm really confused on how to do this, if someone could help, I'd appreciate it, thanks.
This can be done this way:
create a stream from the pairs to concatenate first/second values using Stream::flatMap
count the occurrences - build an intermediate map <Obj, Integer> using Collectors.groupingBy + Collectors.summingInt (to keep integer)
create an inverse map <Integer, List> from the stream of the entries in the intermediate map using Collectors.groupingBy + Collectors.mapping
Optionally, if an order in the resulting map is critical, a LinkedHashMap may be created from the entries of the intermediate frequency map sorted by value.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
return lf.stream() // Stream<Pair>
.flatMap(p -> Stream.of(p.getKey(), p.getValue())) // Stream<Obj>
.collect(Collectors.groupingBy(
obj -> obj,
Collectors.summingInt(obj -> 1)
)) // Map<Obj, Integer>
.entrySet()
.stream() // Stream<Map.Entry<Obj, Integer>>
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.groupingBy(
Map.Entry::getValue, // frequency is key
LinkedHashMap::new,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
)); // Map<Integer, List<Obj>>
}
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));
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 a Multimap structure, Map<String, Set<String>> as input. I want to group entries of this map if any two sets of entry values have a common element. Output should be of the format Map<Set<String>, Set<String>> where each key will be a group of keys from the input map.
eg. given this input:
A -> [1,2]
B -> [3,4]
C -> [5,6]
D -> [1,5]
Output:
[A,C,D] -> [1,2,5,6]
[B] -> [3,4]
Here A & D have 1 as common element, C & D have 5 as common element. So A, C, D are merged into one key.
There are lots of ways you can solve this. One that I like (assuming you are using Java 8) is to implement this as a collector for a Map.Entry stream. Here's a possible implementation:
public class MapCollector {
private final Map<Set<String>,Set<Integer>> result = new HashMap<>();
public void accept(Map.Entry<String,Set<Integer>> entry) {
Set<String> key = new HashSet<>(Arrays.asList(entry.getKey()));
Set<Integer> value = new HashSet<>(entry.getValue());
Set<Set<String>> overlapKeys = result.entrySet().stream()
.filter(e -> e.getValue().stream().anyMatch(value::contains))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
overlapKeys.stream().forEach(key::addAll);
overlapKeys.stream().map(result::get).forEach(value::addAll);
result.keySet().removeAll(overlapKeys);
result.put(key, value);
}
public MapCollector combine(MapCollector other) {
other.result.forEach(this::accept);
return this;
}
public static Collector<Map.Entry<String, Set<Integer>>, MapCollector, Map<Set<String>,Set<Integer>>> collector() {
return Collector.of(MapCollector::new, MapCollector::accept, MapCollector::combine, c -> c.result);
}
}
This can be used as follows:
Map<Set<String>,Set<Integer>> result = input.entrySet().stream()
.collect(MapCollector.collector());
Most of the work is done in the accept method. It finds all overlapping sets and moves them to the new map entry. It supports parallel streams which could be useful if your map is massive.