java8 reduce with useless combiner - java

I need to apply a list of regex to a string, so I thought to use java8 map reduce:
List<SimpleEntry<String, String>> list = new ArrayList<>();
list.add(new SimpleEntry<>("\\s*\\bper\\s+.*$", ""));
list.add(new SimpleEntry<>("\\s*\\bda\\s+.*$", ""));
list.add(new SimpleEntry<>("\\s*\\bcon\\s+.*$", ""));
String s = "Tavolo da cucina";
String reduced = list.stream()
.reduce(s, (v, entry) -> v.replaceAll(entry.getKey(), entry.getValue()) , (c, d) -> c);
Actually this code may be is not very beautiful, but it works. I know this cannot be parallelised and for me is ok.
Now my question is: is there any chance with Java8 (or higher version) to write something more elegant? I mean also avoiding to add the useless combiner function.

Inspired by Oleksandr's comment and Holger I wrote this
String reduced = list.stream()
.map(entry->
(Function<String, String>) v -> v.replaceAll(entry.getKey(), entry.getValue()))
.reduce(Function.identity(), Function::andThen)
.apply(s);
This also reduce all entries to a function composition.

Here's another, interesting approach: reduce all entries to a function composition, then apply that composed function on the original input:
String result = list.stream()
.map(entry ->
(Function<String, String>) text ->
text.replaceAll(entry.getKey(), entry.getValue()))
//following op also be written as .reduce(Function::compose) (see comment by Eugene)
.reduce((f1, f2) -> f1.andThen(f2)) //compose functions
.map(func -> func.apply(s)) //this basically runs all `replaceAll`
.get();
The result of this is your expected string. While this function composition is not intuitive, it nonetheless seems to fit the idea that your original list is in fact a sort of "transformation logic" chain.

Related

Best way to replace nested loop concatenate string using java stream

I have a list of names and a list of versions. I want to get all permutations which are constructed by concatenating the string from two lists. I am using two for loop to do this but I want to switch to a more functional style approach. Here is my solution:
List<String> names = new ArrayList<>();
List<String> versions = new ArrayList<>();
List<String> result = new ArrayList<>();
names.forEach(name -> versions.stream().map(version -> result.add(name.concat(version))));
Is there a better way to do it?
You are looking for the "Cartesian Product" of names and versions — basically the return set/list from the aforementioned sets/lists.
final Stream<List<String>> result = names.stream()
.flatMap(s1 -> versions.stream().flatMap(s2 -> Stream.of(Arrays.asList(s1, s2))));
result.forEach(System.out::println);
Keep in mind that operation is super expensive. Google's Guava have this implemented also under com.google.common.collect.Sets.cartesianProduct(s1, s2).
You should look forward to use flatMap while streaming over names and then performing map operation further correctly as:
List<String> result = names.stream() // for each name
.flatMap(name -> versions.stream() // for each version
.map(version -> name.concat(version))) // concat version to the name
.collect(Collectors.toList()); // collect all such names
Or a bit tidier:
final List<String> result = names.stream() // Stream the Names...
.flatMap(name -> versions.stream() // ...together with Versions.
.map (version -> name.concat(version))) // Combine Name+Version
.collect(Collectors.toList()); // & collect in List.

How do I use Java 8 groupingBy to collect into a list of a different type?

I'm trying to take a map of type A -> A, and group it into a map of A to List<A>. (Also reversing the key-value relationship, but I don't think that is necessarily relevant).
This is what I have now:
private static Map<Thing, List<Thing>> consolidateMap(Map<Thing, Thing> releasedToDraft) {
// Map each draft Thing back to the list of released Things (embedded in entries)
Map<Thing, List<Map.Entry<Thing, Thing>>> draftToEntry = releasedToDraft.entrySet().stream()
.collect(groupingBy(
Map.Entry::getValue,
toList()
));
// Get us back to the map we want (Thing to list of Things)
return draftToEntry.entrySet().stream()
.collect(toMap(
Map.Entry::getKey,
ThingReleaseUtil::entriesToThings
));
}
private static List<Thing> entriesToThings(Map.Entry<Thing, List<Map.Entry<Thing, Thing>>> entry) {
return entry.getValue().stream()
.map(Map.Entry::getKey)
.collect(toList());
}
I'd like to do this in a single statement, and I feel like it must be possible to transform the Map<Thing, List<Map.Entry<Thing, Thing>>> to Map<Thing, List<Thing>> as part of the groupingBy operation.
I've tried using reducing(), custom collectors, everything I can find; but I'm stymied by the lack of complex examples out there, and the fact that the few similar ones I can find have List.of(), which doesn't exist in Java 8 (Collections.singletonList() does not appear to be a good replacement).
Could someone help me with what is probably the obvious?
Must be on the line of
private static Map<Thing, List<Thing>> consolidateMap(Map<Thing, Thing> releasedToDraft) {
return releasedToDraft.entrySet().stream()
.collect(groupingBy(
Map.Entry::getValue,
mapping(Map.Entry::getKey, toList())
));
}

Group, Collectors, Map (Int to String), Map (Map to Object)

This is a continuation of my previous question at Group, Sum byType then get diff using Java streams.
As suggested, I should post as a separate thread instead of updating the original one.
So with my previous set of question, I have achieved that, and now, with the continuation.
Background:
I have the following dataset
Sample(SampleId=1, SampleTypeId=1, SampleQuantity=5, SampleType=ADD),
Sample(SampleId=2, SampleTypeId=1, SampleQuantity=15, SampleType=ADD),
Sample(SampleId=3, SampleTypeId=1, SampleQuantity=25, SampleType=ADD),
Sample(SampleId=4, SampleTypeId=1, SampleQuantity=5, SampleType=SUBTRACT),
Sample(SampleId=5, SampleTypeId=1, SampleQuantity=25, SampleType=SUBTRACT)
Sample(SampleId=6, SampleTypeId=2, SampleQuantity=10, SampleType=ADD),
Sample(SampleId=7, SampleTypeId=2, SampleQuantity=20, SampleType=ADD),
Sample(SampleId=8, SampleTypeId=2, SampleQuantity=30, SampleType=ADD),
Sample(SampleId=9, SampleTypeId=2, SampleQuantity=15, SampleType=SUBTRACT),
Sample(SampleId=10, SampleTypeId=2, SampleQuantity=35, SampleType=SUBTRACT)
I am currently using this:
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getTypeId,
Collectors.summingInt(
sample -> SampleType.ADD.equalsIgnoreCase(sample.getSampleType())
? sample.getSampleQuantity() :
-sample.getSampleQuantity()
)));
And also this
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getSampleTypeId,
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
as the accepted answer to get the desired output to group in a Map<Long, Integer>:
{1=15, 2=10}
With that, I was wondering, if this could be expanded into something more.
First, how could I have it return as a Map<String, Integer> instead of the original Map<Long, Integer>. Basically, for the SampleTypeId; 1 refers to HELLO, 2 refers to WORLD.
So I would need like a .map (or maybe other function) to transform the data from 1 to HELLO and 2 to WORLD by calling a function say convertType(sampleTypeId)?. So the expected output would then be {"HELLO"=15, "WORLD"=10}. Is that right? How should I edit the current suggested solution to this?
Lastly, I would like to know if it is also possible to return it to a Object instead of a Map. So let's say I have a Object; SummaryResult with (String) name and (int) result. So it returns a List<SummaryResult> instead of the original Map<Long, Integer>. How can I use the .map (or other) feature to do this? Or is there other way to doing so? The expected output would be something along this line.
SummaryResult(name="hello", result=15),
SummaryResult(name="world", result=10),
Would really appreciate it with the explanation in steps as given previously by #M. Prokhorov.
Update:
After updating to
sampleList.stream()
.collect(Collectors.groupingBy(sample -> convertType(sample.getSampleTypeId()),
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
private String convertType(int id) {
return (id == 1) ? "HELLO" : "WORLD";
}
For first part, considering you have somewhere the method
String convertType(int typeId)
You simply need to change first classifier from this
groupingBy(SampleType::getTypeId)
to this
groupingBy(sample -> convertType(sample.getTypeId()))
Everything else remains the same.
Latter type is a little trickier, and technically doesn't benefit from it being a stream-related solution at all.
What you need is this:
public List<SummaryResult> toSummaryResultList(Map<String, Integer> resultMap) {
List<SummaryResult> list = new ArrayList<>(resultMap.size());
for (Map.Entry<String, Integer> entry : resultMap.entrySet()) {
String name = entry.getKey();
Integer value = entry.getValue();
// replace below with construction method you actually have
list.add(SummaryResult.withName(name).andResult(value));
}
return list;
}
You can use this as part of collector composition, where your whole collector will get wrapped into a collectingAndThen call:
collectingAndThen(
groupingBy(sample -> convertType(sample.getTypeId()),
collectingAndThen(
groupingBy(Sample::getSampleType,
summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))),
result -> toSummaryResultList(result))
However, as you can see, it is the whole collector that gets wrapped, so there is no real benefit in my eyes to the above version to a simpler and easier to follow (at least to me) version below that uses an intermediate variable, but isn't so much of a wall of code:
// do the whole collecting thing like before
Map<String, Integer> map = sampleList.stream()
.collect(Collectors.groupingBy(sample -> convertType(sample.getTypeId()),
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
// return the "beautified" result
return toSummaryResultList(map);
Another point to consider in above is: convertType method will be called as many times as there are elements in sampleList, so if convertType call is "heavy" (for example, uses database or IO), then it's better to call it as part of toSummaryResultList conversion, not as stream element classifier. In which case you will be collecting from map of type Map<Integer, Integer> still, and using convertType inside the loop. I will not add any code with this in consideration, as I view this change as trivial.
You could indeed use a map() function
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getSampleTypeId,
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))))
.entrySet()
.stream()
.map(entry->new SummaryResult(entry.getKey()),entry.getValue())
.collect(Collectors.toList());
ToIntFunction<Sample> signedQuantityMapper= sample -> sample.getQuantity()
* (sample.getType() == Type.ADD ? 1 : -1);
Function<Sample, String> keyMapper = s -> Integer.toString(s.getTypeId());
Map<String, Integer> result = sampleList.stream().collect(
Collectors.groupingBy(
keyMapper,
Collectors.summingInt(signedQuantityMapper)));

How to convert a Collection<Pair<K, Collection<V>> to a List<MyObject<K,V>> using Java stream API?

My traditional code would look like this:
List<MyObject> transform(Collection<java.util.Map.Entry<String, List<String>>> input) {
List<MyObject> output = new LinkedList<>();
for (Entry<String, List<String>> pair : input) {
for (String value : pair.getValue()) {
output.add(new MyObject(pair.getKey(), value));
}
}
return output;
}
Can I do the same with lambda expressions? I’ve tried around, but I don’t get it. The outer collection is unsorted, but the List<String> is sorted. The result objects may return in the result list without any order, with the exception that objects created from the same key String should follow each other to preserve the order of the value. Is this at all possible?
input.stream()
.flatMap(e -> e.getValue()
.stream()
.map(v -> new MyObject(e.getKey(), v)))
.collect(Collectors.toCollection(LinkedList::new));
You can use streams and Stream.flatMap as in this answer, however I think that the code is much clearer if you stick to loops, either traditional ones as in your question, or modern ones:
List<MyObject> output = new LinkedList<>();
input.forEach(pair -> pair.getValue()
.forEach(value -> output.add(new MyObject(pair.getKey(), value))));
By the way, I'd use an ArrayList instead of a LinkedList.

Java Stream.collect using groupingBy

I have a small snippet of code where I want to group the results by a combination of 2 properties of the type in the stream. After appropriate filtering, I do a map where I create an instance of a simple type that holds those 2 properties (in this case called AirportDay). Now I want to group them together and order them descending by the count. The trouble I am having is coming up with the correct arguments for the groupingBy method. Here is my code so far:
final int year = getYear();
final int limit = getLimit(10, 1, 100);
repository.getFlightStream(year)
.filter(f -> f.notCancelled())
.map(f -> new AirportDay(f.getOriginAirport(), f.getDate()))
.collect(groupingBy( ????? , counting())) // stuck here
.entrySet()
.stream()
.sorted(comparingByValue(reverseOrder()))
.limit(limit)
.forEach(entry -> {
AirportDay key = entry.getKey();
printf("%-30s\t%s\t%,10d\n",
key.getAirport().getName(),
key.getDate(),
entry.getValue()
);
});
My first instinct was to pass AirportDay::this but that obviously doesn't work...
I'd appreciate any assistance you can provide in coming up with a solution to the above problem.
-Tony
If you want to group by AirportDay, provide the function to create the key to groupingBy:
repository.getFlightStream(year)
.filter(f -> f.notCancelled())
.collect(groupingBy(f -> new AirportDay(f.getOriginAirport(), f.getDate()), counting()))
Note: The AirportDay class must implement sensible equals() and hashCode() methods for this to work.

Categories