Java 8 stream.collect(Collectors.toMap()) analog in kotlin - java

Suppose I have a list of persons and would like to have Map<String, Person>, where String is person name. How should I do that in kotlin?

Assuming that you have
val list: List<Person> = listOf(Person("Ann", 19), Person("John", 23))
the associateBy function would probably satisfy you:
val map = list.associateBy({ it.name }, { it.age })
/* Contains:
* "Ann" -> 19
* "John" -> 23
*/
As said in KDoc, associateBy:
Returns a Map containing the values provided by valueTransform and indexed by keySelector functions applied to elements of the given array.
If any two elements would have the same key returned by keySelector the last one gets added to the map.
The returned map preserves the entry iteration order of the original array.
It's applicable to any Iterable.

Many alternatives in Kotlin :)
val x: Map<String, Int> = list.associate { it.name to it.age }
val y: Map<String, Int> = list.map { it.name to it.age }.toMap()
var z: Map<String, Int> = list.associateBy({ it.name }, { it.age })
The best option for this particular case IMO is the first as Lambdas are not needed but simple transformation.

Related

Correct Data structure to be used for the given problem - Java

I want to process the subset of values in Java.
The scenario is like
Example 1:
IDA - {x, y}
IDB - {x, y, z}
IDC - {z}
This should be clubbed in a JSON as follows,
{data : {x,y}, issuer: {IDA, IDB }}
{data : {z}, issuer: {IDB, IDC }}
Example 2:
IDA - {x, y, z}
IDB - {y, z}
IDC - {z}
JSON structure should be
{data : {y, z}, issuer: {IDA, IDB }}
{data : {x}, issuer: {IDA }}
{data : {z}, issuer: {IDC }}
Here, I would like to have idea about using the correct data structure in Java.
Note: In the given example, the data is taken with three entries. But , there may be 'n' number of entries.
To arrange the data in the desired way, we can use a couple of intermediate maps.
Also, we would need a few custom types to perform transformations and to store the final result that would be serialized into JSON. I'll use Java 16 records for that purpose (but they could also be implemented as classes).
Map<provider, List<data>>
| + intermediate transformation:
v entry -> group of objects `provider + data`
Map<data, Set<provider>>
|
v
Map<Set<provider>, List<data>>
|
v
List of custom type that would be serialized into JSON
These are the main steps:
Split every entry of the initial map into a group of custom objects, each holding a reference only one reference to data and provider;
Group these objects by data and form the first auxiliary map of type Map<String, Set<String>> (providers by data).
Iterate over the entries of the newly created map and rearrange the data by grouping the entries by value, which is of type Set<String> (set of providers) into the second auxiliary map of type Map<Set<String>, List<String>>. The values of the second map would contain data-strings mapped to identical sets of providers (set is used to make the comparison not sensitive to ordering of providers).
Note that in this case it's safe to use a mutable collection (which can be created via Collectors.toSet() - see the code below) as a map-key because we need it only to perform intermediate transformation, and we're not storing references neither to the keys, no to the map. But if the reader would want to use a piece of the solution provided below to generate a map which would be stored as a field and passed around, then to avoid accidental changes the key should be represented by an immutable collection. Therefore, to make the solution more versatile, I've Collectors.toUnmodifiableSet().
The final step is to generate a List of objects that would be serialized into JSON. For that, we need to iterate over the second auxiliary map and turn each entry into a custom object.
The data is ready to be marshaled into JSON. For demo-purposes, I've used Jackson (reader can apply any other tool of their choice).
Custom objects:
public record DataProvider(String data, String provider) {}
public record DataProviders(Set<String> data, List<String> providers) {}
The map logic:
Map<String, List<String>> dataByProvider = Map.of(
"localIDA", List.of("x", "y"),
"localIDB", List.of("x", "y", "z"),
"localIDC", List.of("z")
);
List<DataProviders> dataProviders = dataByProvider.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(data -> new DataProvider(data, entry.getKey()))
)
.collect(Collectors.groupingBy(
DataProvider::data,
Collectors.mapping(DataProvider::provider,
Collectors.toUnmodifiableSet())
))
.entrySet().stream()
.collect(
HashMap::new,
(Map<Set<String>, List<String>> map, Map.Entry<String, Set<String>> entry) ->
map.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()),
(left, right) ->
right.forEach((k, v) -> left.merge(k, v, (oldV, newV) -> {
oldV.addAll(newV);
return oldV;
}))
)
.entrySet().stream()
.map(entry -> new DataProviders(entry.getKey(), entry.getValue()))
.toList(); // for Java 16+ or collect(Collectors.toList())
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT); // to make the output look nicely (can be omitted)
String json = mapper.writeValueAsString(dataProviders);
System.out.println(json);
Output:
[ {
"data" : [ "localIDB", "localIDA" ],
"providers" : [ "x", "y" ]
}, {
"data" : [ "localIDB", "localIDC" ],
"providers" : [ "z" ]
} ]

Convert Map<Integer, List<Strings> to Map<String, List<Integer>

I'm having a hard time converting a Map containing some integers as keys and a list of random strings as values.
E.g. :
1 = ["a", "b", "c"]
2 = ["a", "b", "z"]
3 = ["z"]
I want to transform it into a Map of distinct strings as keys and lists the integers as values.
E.g. :
a = [1, 2]
b = [1, 2]
c = [1]
z = [2,3]
Here's what I have so far:
Map<Integer, List<String>> integerListMap; // initial list is already populated
List<String> distinctStrings = new ArrayList<>();
SortedMap<String, List<Integer>> stringListSortedMap = new TreeMap<>();
for(Integer i: integers) {
integerListMap.put(i, strings);
distinctStrings.addAll(strings);
}
distinctStrings = distinctStrings.stream().distinct().collect(Collectors.toList());
for(String s : distinctStrings) {
distinctStrings.put(s, ???);
}
Iterate over your source map's value and put each value into the target map.
final Map<String, List<Integer>> target = new HashMap<>();
for (final Map.Entry<Integer, List<String>> entry = source.entrySet()) {
for (final String s : entry.getValue()) {
target.computeIfAbsent(s, k -> new ArrayList<>()).add(entry.getKey());
}
}
Or with the Stream API by abusing Map.Entry to build the inverse:
final Map<String, List<Integer>> target = source.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(s -> Map.entry(s, e.getKey()))
.collect(Collectors.groupingBy(e::getKey, Collectors.mapping(e::getValue, Collectors.toList())));
Although this might not be as clear as introducing a new custom type to hold the inverted mapping.
Another alternative would be using a bidirectial map. Many libraries come implementations of these, such as Guava.
There's no need to apply distinct() since you're storing the data into the Map and keys are guaranteed to be unique.
You can flatten the entries of the source map, so that only one string (let's call it name) and a single integer (let's call it number) would correspond to a stream element, and then group the data by string.
To implement this problem using streams, we can utilize flatMap() operation to perform one-to-many transformation. And it's a good practice to define a custom type for that purpose as a Java 16 record, or a class (you can also use a Map.Entry, but note that approach of using a custom type is more advantages because it allows writing self-documenting code).
In order to collect the data into a TreeMap you can make use of the three-args version of groupingBy() which allows to specify mapFactory.
record NameNumber(String name, Integer number) {}
Map<Integer, List<String>> dataByProvider = Map.of(
1, List.of("a", "b", "c"),
2, List.of("a", "b", "z"),
3, List.of("z")
);
NavigableMap<String, List<Integer>> numbersByName = dataByProvider.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(name -> new NameNumber(name, entry.getKey()))
)
.collect(Collectors.groupingBy(
NameNumber::name,
TreeMap::new,
Collectors.mapping(NameNumber::number, Collectors.toList())
));
numbersByName.forEach((name, numbers) -> System.out.println(name + " -> " + numbers));
Output:
a -> [2, 1]
b -> [2, 1]
c -> [1]
z -> [3, 2]
Sidenote: while using TreeMap it's more beneficial to use NavigableMap as an abstract type because it allows to access methods like higherKey(), lowerKey(), firstEntry(), lastEntry(), etc. which are declared in the SortedMap interface.

How to flatten list of list values in groupBy in Kotlin

I have an object structure
data class File(val travelBatch: List<TravelBatch>) {
data class TravelBatch(
val currency: String,
val transactions: List<Transaction>
)
}
I want to have a map of currency to transactions. The below code I tried gives
Map<String, List<List<Transaction>> I want Map<String, List<Transaction>
file.travelBatch.groupBy({it.currency}, {it.transactions})
Need help to flatten the values in the map in kotlin?
You can use mapValues
val result = file.travelBatch
.groupBy({ it.currency }, { it.transactions })
.mapValues { it.value.flatten() }

Java Stream compare and filter entries from Map<String, Map<String, List<String>>>

I have the following class:
public class SomeObject {
private String id;
private String parentId;
private String type;
//constructor,getters,setters
}
And the following use case:
The field values are not unique. I have a List of SomeObject. First I want to know which SomeOjects share the same parentId and secondly which of those share the same type. First I wanted to group them into the following structure:
Map<String, Map<String, List<String>>>
The key of the first map is the parentId and the value is another map. The key of the second map is the type and the value of the second map is a list of ids from the SomeObjects.
I was able to do this as follows:
Map<String, Map<String, List<String>>> firstTry =
SomeObjects.stream()
.collect(
groupingBy(
SomeObject::getParentId,
groupingBy(
SomeObject::getType,
mapping(SomeObject::getId, toList()))));
And now comes the part where I need some help:
I now want to filter this created map as follows:
Lets assume I have 3 parentId keys which each then have a map with two keys: type1 and type2. (and 2 lists of ids as values)
If the list of ids from type2 contains more/less/different ids than the list of ids from type1, then I want to delete/filter out their parentId entry. And I want to do that for each parentId.
Is there any way with streams to cleanly achieve this?
If you need to retain only those parentId which have the same ids per type, it can be done by converting lists of ids into set and checking the set size:
List<SomeObject> list = Arrays.asList(
new SomeObject("id1", "p1", "type1"),
new SomeObject("id1", "p1", "type2"),
new SomeObject("id2", "p2", "type1"),
new SomeObject("id3", "p2", "type1"),
new SomeObject("id2", "p2", "type2"),
new SomeObject("id4", "p3", "type1"),
new SomeObject("id4", "p3", "type2"),
new SomeObject("id5", "p3", "type2")
);
//.. building firstTry as in the initial code snippet
System.out.println(firstTry);
firstTry.entrySet().stream()
.filter(e -> new HashSet(e.getValue().values()).size() == 1)
.forEach(System.out::println);
Output:
{p1={type2=[id1], type1=[id1]}, p2={type2=[id2], type1=[id2, id3]}, p3={type2=[id4, id5], type1=[id4]}}
p1={type2=[id1], type1=[id1]}
Some points I would use to improve the code before the answere:
I would leave the SomeObject pointer instead of the String literal in the map. Not only they are gonna be more memory efficient most of the time (8 bytes fixed vs 2*character bytes String) but also much more convenient to access the data.
I would also make the type String an enum type.
But getting to the core of the question, i'm sorry.
Lets assume I have 3 parentId keys which each then have a map with two keys: type1 and type2. (and 2 lists of ids as values)
{
'p1': {
'type1':['1','2','uuid-random-whatever'],
'type2':['asdsad']
},
'p2': {
'type1':['1','2'],
'type2':['asdsad','i','want','more','power']
},
'p3': {
'type1':['2'],
'type2':['2']
}
}
Something like this
So for the filtering itself
Map<String, Map<String, List<String>>> theMap
= buildMapExample();
Predicate<Map.Entry<String, Map<String, List<String>>>> filterType2LessElementsType1 =
(Map.Entry<String, Map<String, List<String>>> entry) ->
entry.getValue().get("type2").size() < entry.getValue().get("type1").size();
Predicate<Map.Entry<String, Map<String, List<String>>>> filterType2MoreElementsType1 =
(Map.Entry<String, Map<String, List<String>>> entry) ->
entry.getValue().get("type2").size() > entry.getValue().get("type1").size();
Predicate<Map.Entry<String, Map<String, List<String>>>> filterType2SameElementsType1 =
(Map.Entry<String, Map<String, List<String>>> entry) ->
(new HashSet<>(entry.getValue().get("type2")))
.equals(new HashSet<>(entry.getValue().get("type1")));
theMap = theMap.entrySet().stream()
.filter(
// Choose your lambda for more/less/different. for example
filterType2LessElementsType1
)
.collect(
Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)
);
printMap(theMap);
This code would work. I left out the building/printing to not make this larger as it should. Try it out

Java Stream Generate Map from List of Objects

I have a class like this.
public class Foo {
private String prefix;
private String sector;
private int count;
}
Given a foo list:
//Args: prefix, sector, count
fooList.add(new Foo("44",,"RowC", 1 ));
fooList.add(new Foo("1",,"Rowa", 1 ));
fooList.add(new Foo("1",,"RowB", 1 ));
fooList.add(new Foo("1",,"Rowa", 1 ));
And I need to return the request an object sorted by Prefix asc like this:
{
"1": {
"Rowa": "2",
"RowB": "1"
},
"44": {
"RowC": "1"
}
}
So the problem is:
I have to group the list by the prefix, and then show, every sector and the count(*) of items on the list with the same row and sector.
The far that I got is using stream like this:
fooList.stream()
.collect(Collectors.groupingBy(
Foo::getPrefix,
Collectors.groupingBy(
Foo::getSector,
Collectors.mapping(Foo::getSector , Collectors.counting())
)
));
The problem is, that the code above, is that the count is a Long, and I need to return as a String.
I've tried with .toString but it gives me an error (Can assign java.lang.String to java.util.stream.Collector).
UPDATE
With the help of Andreas and Naman, now I can map count as String.
I just need it sorted by Prefix.
Can anyone help me?
You were almost there, just replace the Collectors.mapping line with:
Collectors.summingInt(Foo::getCount))
As in:
List<Foo> fooList = new ArrayList<>();
fooList.add(new Foo("44", "RowC", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
fooList.add(new Foo("1", "RowB", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
Map<String, Map<String, String>> result = fooList.stream().collect(
Collectors.groupingBy(
Foo::getPrefix,
TreeMap::new, // remove if prefix sorting not needed
Collectors.groupingBy(
Foo::getSector,
() -> new TreeMap<>(Collator.getInstance()), // remove if sector sorting not needed
Collectors.collectingAndThen(
Collectors.summingInt(Foo::getCount),
String::valueOf
)
)
)
);
System.out.println(result); // prints: {1={Rowa=2, RowB=1}, 44={RowC=1}}
Notice the TreeMap constructors added to the groupingBy() calls, which ensures that the maps are sorted. The first is sorting lexicographically, while the second sorts according to spoken language, i.e. upper- vs lowercase doesn't affect order.
Is this what you need?
Code :
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.summingInt;
public class Main {
public static void main(String [] args){
List<Foo> fooList = new ArrayList<>();
fooList.add(new Foo("44", "RowC", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
fooList.add(new Foo("1", "RowB", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
Map<String, Map<String, Integer>> result = grouper(fooList);
result.forEach( (k,v) -> System.out.printf("%s\n%s\n", k,v) );
}
/* group the list by the prefix, and then show, every sector and the
* count(*) of items on the list with the same row and sector.*/
public static Map<String, Map<String, Integer>> grouper(List<Foo> foos){
//Map<prefix, Map<sector, count>
Map<String, Map<String, Integer>> result = foos.stream()
.collect(
//Step 1 - group by prefix. Outer map key is prefix.
groupingBy( Foo::getPrefix,
//Use a tree map which wil be sorted by its key, i.e prefix.
TreeMap::new,
//Step 2 - group by sector.
groupingBy( Foo::getSector,
//Step 3 - Sum the Foo's in each sector in each prefix.
summingInt(Foo::getCount)
)
)
);
return result;
}
}
Output :
1
{Rowa=2, RowB=1}
44
{RowC=1}
PS - I referred to this tutorial to answer your question. I referred to the examples in "2.5. Grouping by Multiple Fields" and "2.7. Getting the Sum from Grouped Results". The next step was to find out how to also order by the key of a map while grouping which I got from "2.11. Modifying the Return Map Type" and also checked it here.

Categories