Show all the longest words from finite Stream - java

I have to find all longest words from given file using Streams API. I did it in few steps but looking for some "one liner", actually I processing whole file two times, first to find max length of word and second for comparing all to max length, assuming its not the best for performance ; P Could someone help me? Just look at code:
public class Test {
public static void main(String[] args) throws IOException {
List<String> words = Files.readAllLines(Paths.get("alice.txt"));
OptionalInt longestWordLength = words.stream().mapToInt(String::length).max();
Map<Integer, List<String>> groupedByLength = words.stream().collect(Collectors.groupingBy(String::length));
List<String> result = groupedByLength.get(longestWordLength.getAsInt());
}
}
I wish to make it straight:
List<String> words = Files.readAllLines(Paths.get("alice.txt"));
List<String> result = // code
File contains just one word per line, anyway it's not important - question is about the right stream code.

Instead of just keeping the largest length, you could collect the words into a map from their length to the words, and then just take the longest one:
List<String> longestWords =
Files.lines(Paths.get("alice.txt"))
.collect(Collectors.groupingBy(String::length))
.entrySet()
.stream()
.sorted(Map.Entry.<Integer, List<String>> comparingByKey().reversed())
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
EDIT:
As Malte Hartwig noted, using max on the streamed map is much more elegant (and probably faster):
List<String> longestWords =
Files.lines(Paths.get("alice.txt"))
.collect(Collectors.groupingBy(String::length))
.entrySet()
.stream()
.max(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.orElse(null);
EDIT2:
There's a built-in inefficiency in both the above solutions, as they both build a map a essentially store the lengths for all the strings in the file instead of just the longest ones. If performance is more important than elegance in your usecase, you could write your own Collector to just preserve the longest strings in list:
private static int stringInListLength(List<String> list) {
return list.stream().map(String::length).findFirst().orElse(0);
}
List<String> longestWords =
Files.lines(Paths.get("alice.txt"))
.collect(Collector.of(
LinkedList::new,
(List<String> list, String string) -> {
int stringLen = string.length();
int listStringLen = stringInListLength(list);
if (stringLen > listStringLen) {
list.clear();
}
if (stringLen >= listStringLen) {
list.add(string);
}
},
(list1, list2) -> {
int list1StringLen = stringInListLength(list1);
int list2StringLen = stringInListLength(list2);
if (list1StringLen > list2StringLen) {
return list1;
}
if (list2StringLen > list1StringLen) {
return list2;
}
list1.addAll(list2);
return list1;
}
));

reduce will help you:
Optional<String> longest = words.stream()
.reduce((s1, s2) -> {
if (s1.length() > s2.length())
return s1;
else
return s2;
});
In case the Stream is empty it will return an Optional.empty
In case you want the list of all the words that have the maximum length this piece will help you:
Optional<List<String>> longest = words.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.toList()
))
.entrySet()
.stream()
.reduce(
(entry1, entry2) -> {
if (entry1.getKey() > entry2.getKey())
return entry1;
else
return entry2;
}
)
.map(Map.Entry::getValue);

iterate over the map keys to find the longest wordlength

Related

Iterate over two Lists of Lists with IntStream instead of streams

I am trying to use streams in order to iterate over two lists of lists in order to verify if the inner lists sizes are the same for the same index. I have managed to achieve this using streams, but I have to rewrite using an IntStream and mapToObj.
My current approach is:
List<List<String>> a = config.getStrips();
List<List<Integer>> b = slotMachineConfig.getWeights();
a.stream()
.filter(first ->
b.stream()
.allMatch(second -> second.size() == first.size())
)
.findFirst()
.orElseThrow(InvalidConfigException::new);
The problem is that I cannot be sure that the sizes will correspond for the big lists, so I have to rewrite this using IntStream and also using indexes for each list.
What I have so far, but does not work looks like this, I am trying to write a "validate" function in order to verify the inner lists, but it seems like I get an error there saying "no instance of type variable U exist so that void conforms to U".
IntStream.range(0, a.size())
.mapToObj(i -> validate(i, a.get(i), b.get(i)))
.findFirst()
.orElseThrow(SlotMachineInvalidConfigException::new);
public void validate(int index, List<String> firstList, List<Integer> secondList) {
How can I rewrite my method using IntStream and mapToObj, can anyone help me?
You have the right idea but you don't really need a separate validation function if you are just comparing sizes. Here's a working example that supports any list types:
public class ListSizeMatcher {
public <T,S> boolean sizeMatches(List<List<T>> list1, List<List<S>> list2) {
return list1.size() == list2.size()
&& IntStream.range(0, list1.size())
.allMatch(i -> list1.get(i).size() == list2.get(i).size());
}
public static void main(String[] args) {
ListSizeMatcher matcher = new ListSizeMatcher();
System.out.println(matcher.sizeMatches(List.of(List.of(1)), List.of(List.of("a"), List.of("b"))));
System.out.println(matcher.sizeMatches(List.of(List.of(1)), List.of(List.of("a", "b"))));
System.out.println(matcher.sizeMatches(List.of(List.of(1, 2)), List.of(List.of("a", "b"))));
}
}
Note that from a design perspective if each item in the list matches the corresponding item in a separate list you'd be better off creating a single class that contains both items.
If I understand correctly, I think something like this would work:
List<List<String>> a = config.getStrips();
List<List<Integer>> b = slotMachineConfig.getWeights();
if (a.size() != b.size()) throw new InvalidConfigException();
boolean allTheSame = IntStream.range(0, a.size())
.map(i -> a.get(i).size() - b.get(i).size())
.allMatch(diff -> diff == 0);
if (!allTheSame) throw new InvalidConfigException();
For the record, your validate function returns void but I'll assume it was meant to return a boolean
here is a more compact version
List<List<String>> a = new LinkedList<>();
List<List<Integer>> b = new LinkedList<>();
boolean match = IntStream.range(0, a.size())
.mapToObj(i -> a.get(i).size() == b.get(i).size())
.reduce(Boolean::logicalAnd).orElseThrow(InvalidConfigException::new);
if (!match) {
throw new InvalidConfigException();
}
Alternative:
List<List<String>> a = new LinkedList<>();
List<List<Integer>> b = new LinkedList<>();
if (IntStream.range(0, a.size()).filter(i -> a.get(i).size() != b.get(i).size()).count() > 0){
throw new InvalidConfigException();
};
At the end of the day it only takes 1 to be different and fail.
The error means that the validate method cannot be void and it is expected to return some valid value (possibly boolean).
If the inner lists are supposed to have the equal sizes to be valid, the check may look as follows:
// assuming the sizes of outer lists are equal
boolean allSizesEqual = IntStream.range(0, a.size())
.allMatch(i -> a.get(i).size() == b.get(i).size());
if (!allSizesEqual) {
throw new InvalidConfigException("Not all sizes are valid");
}
If there's a need to find specific indexes where a discrepancy is detected:
List<Integer> badIndexes = IntStream.range(0, a.size())
.filter(i -> a.get(i).size() != b.get(i).size()) // IntStream
.boxed() // Stream<Integer>
.collect(Collectors.toList());
if (!badIndexes.isEmpty()) {
throw new InvalidConfigException("Different indexes found: " + badIndexes);
}
Or validate method could be fixed to return appropriate value for the filter:
boolean allItemsValid = IntStream.range(0, a.size())
.allMatch(i -> listsAreValid(a.get(i), b.get(i)));
if (!allItemsValid) {
throw new InvalidConfigException("Not all entries are valid");
}
public boolean listsAreValid(List<String> innerA, List<Integer> innerB) {
// any advanced logic
return innerA.size() == innerB.size();
}

Concatenating repeating String n times to themself basing on value from Collectors.groupingBy(K, V)

Let's say that I have a method which is finding possible substrings in the String and return the particular one which will occur more than one time, otherwise, it's returning -1. For example for abcdabcd it returns abcd. My current solution is pretty close to the ideal, but I want to return instead of abcd, concatenated occurrence with result abcdabcd basing on Collectors.groupingBy value because according to the Key : Value pairs: "abcd" occurred twice.
public static String StringPeriods(String str) {
List<String> substrings = new ArrayList<>();
for (int i = 0; i < str.length(); i++) {
for (int j = i + 1; j <= str.length(); j++) {
substrings.add(str.substring(i, j));
}
}
return substrings.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.stream()
.filter(stringLongEntry -> stringLongEntry.getValue() > 1)
.map(Entry::getKey)
.//magic here
.findFirst()
.orElse("-1");
}
Moreover, I would avoid reopening stream, instead of using concat() method or another simple solution. I will be grateful for suggestions on how to reach a goal.
You can solve your problem in multiple steps:
// Step 1: group by counting
Map<String, Long> grouping = substrings.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// Step 2: find the max value
Long maxValue = grouping.entrySet().stream()
.max(Map.Entry.comparingByValue())
.get()
.getValue();
// Step 2: filter the entry which have max value and then max length,
// In the end repeat your String maxValue time
return grouping.entrySet()
.stream()
.filter(entry -> entry.getValue() == maxValue)
.max(Map.Entry.comparingByKey(Comparator.comparingInt(String::length)))
.map(entry -> entry.getKey().repeat(maxValue.intValue()))
.get();
I don't get you, when you use orElse("-1"), I think it is useless, just use get() in the end, and if you want to avoid empty strings, then just make a check in the start of your method:
if (str.isEmpty()) {
return "-1";
}
Note: I used repeat which exist in Java11, if you are using an old version, there are many ways to repeat a string.
Or as #Holger mention, you in one shot use:
return substrings.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.stream()
.max(Map.Entry.<String, Long>comparingByValue().thenComparingInt(e -> e.getKey().length()))
.map(entry -> entry.getKey().repeat(entry.getValue().intValue()))
.get();

How to find duplicate values based upon first 10 digits?

I have a scenario where i have a list as below :
List<String> a1 = new ArrayList<String>();
a1.add("1070045028000");
a1.add("1070045028001");
a1.add("1070045052000");
a1.add("1070045086000");
a1.add("1070045052001");
a1.add("1070045089000");
I tried below to find duplicate elements but it will check whole string instead of partial string(first 10 digits).
for (String s:al){
if(!unique.add(s)){
System.out.println(s);
}
}
Is there any possible way to identify all duplicates based upon the first 10 digits of a number & then find the lowest strings by comparing from the duplicates & add in to another list?
Note: Also there will be only 2 duplicates with each 10 digit string code always!!
You may group by a (String s) -> s.substring(0, 10)
Map<String, List<String>> map = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 10)));
map.values() would give you Collection<List<String>> where each List<String> is a list of duplicates.
{
1070045028=[1070045028000, 1070045028001],
1070045089=[1070045089000],
1070045086=[1070045086000],
1070045052=[1070045052000, 1070045052001]
}
If it's a single-element list, no duplicates were found, and you can filter these entries out.
{
1070045028=[1070045028000, 1070045028001],
1070045052=[1070045052000, 1070045052001]
}
Then the problem boils down to reducing a list of values to a single value.
[1070045028000, 1070045028001] -> 1070045028000
We know that the first 10 symbols are the same, we may ignore them while comparing.
[1070045028000, 1070045028001] -> [000, 001]
They are still raw String values, we may convert them to numbers.
[000, 001] -> [0, 1]
A natural Comparator<Integer> will give 0 as the minimum.
0
0 -> 000 -> 1070045028000
Repeat it for all the lists in map.values() and you are done.
The code would be
List<String> result = map
.values()
.stream()
.filter(list -> list.size() > 1)
.map(l -> l.stream().min(Comparator.comparingInt(s -> Integer.valueOf(s.substring(10)))).get())
.collect(Collectors.toList());
A straight-forward loop solution would be
List<String> a1 = Arrays.asList("1070045028000", "1070045028001",
"1070045052000", "1070045086000", "1070045052001", "1070045089000");
Set<String> unique = new HashSet<>();
Map<String,String> map = new HashMap<>();
for(String s: a1) {
String firstTen = s.substring(0, 10);
if(!unique.add(firstTen)) map.put(firstTen, s);
}
for(String s1: a1) {
String firstTen = s1.substring(0, 10);
map.computeIfPresent(firstTen, (k, s2) -> s1.compareTo(s2) < 0? s1: s2);
}
List<String> minDup = new ArrayList<>(map.values());
First, we add all duplicates to a Map, then we iterate over the list again and select the minimum for all values present in the map.
Alternatively, we may add all elements to a map, collecting them into lists, then select the minimum out of those, which have a size bigger than one:
List<String> minDup = new ArrayList<>();
Map<String,List<String>> map = new HashMap<>();
for(String s: a1) {
map.computeIfAbsent(s.substring(0, 10), x -> new ArrayList<>()).add(s);
}
for(List<String> list: map.values()) {
if(list.size() > 1) minDup.add(Collections.min(list));
}
This logic is directly expressible with the Stream API:
List<String> minDup = a1.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 10)))
.values().stream()
.filter(list -> list.size() > 1)
.map(Collections::min)
.collect(Collectors.toList());
Since you said that there will be only 2 duplicates per key, the overhead of collecting a List before selecting the minimum is negligible.
The solutions above assume that you only want to keep values having duplicates. Otherwise, you can use
List<String> minDup = a1.stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(s -> s.substring(0, 10), Function.identity(),
BinaryOperator.minBy(Comparator.<String>naturalOrder())),
m -> new ArrayList<>(m.values())));
which is equivalent to
Map<String,String> map = new HashMap<>();
for(String s: a1) {
map.merge(s.substring(0, 10), s, BinaryOperator.minBy(Comparator.naturalOrder()));
}
List<String> minDup = new ArrayList<>(map.values());
Common to those solutions is that you don’t have to identify duplicates first, as when you want to keep unique values too, the task reduces to selecting the minimum when encountering a minimum.
While I hate doing your homework for you, this was fun. :/
public static void main(String[] args) {
List<String> al=new ArrayList<>();
al.add("1070045028000");
al.add("1070045028001");
al.add("1070045052000");
al.add("1070045086000");
al.add("1070045052001");
al.add("1070045089000");
List<String> ret=new ArrayList<>();
for(String a:al) {
boolean handled = false;
for(int i=0;i<ret.size();i++){
String ri = ret.get(i);
if(ri.substring(0, 10).equals(a.substring(0,10))) {
Long iri = Long.parseLong(ri);
Long ia = Long.parseLong(a);
if(ia < iri){
//a is smaller, so replace it in the list
ret.set(i, a);
}
//it was a duplicate, we are done with it
handled = true;
break;
}
}
if(!handled) {
//wasn't a duplicate, just add it
ret.add(a);
}
}
System.out.println(ret);
}
prints
[1070045028000, 1070045052000, 1070045086000, 1070045089000]
Here's another way to do it – construct a Set and store just the 10-digit prefix:
Set<String> set = new HashSet<>();
for (String number : a1) {
String prefix = number.substring(0, 10);
if (set.contains(prefix)) {
System.out.println("found duplicate prefix [" + prefix + "], skipping " + number);
} else {
set.add(prefix);
}
}

Sorting sets alphabetically, letters in sets separated by commas

public static void main(String[] args) throws IOException
{
HashSet set = new HashSet<String>();
set.add("{}");
set.add("{a}");
set.add("{b}");
set.add("{a, b}");
set.add("{a, c}");
sortedSet(set);
}
public static void sortedSet(HashSet set)
{
List<String> setList = new ArrayList<String>(set);
List<String> orderedByAlpha = new ArrayList<String>(set);
//sort by alphabetical order
orderedByAlpha = (List<String>) setList.stream()
.sorted((s1, s2) -> s1.compareToIgnoreCase(s2))
.collect(Collectors.toList());
System.out.println(orderedByAlpha);
}
I am trying to sort alphabetically but the output I get is this :
[{a, b}, {a, c}, {a}, {b}, {}]
but it should be:
[{a}, {a, b}, {a, c}, {b}, {}]
You're output doesn't match your code. You are showing 2D array lists, but your converting to a 1D arraylist, doesn't make sense.
public static void main(String[] args)
{
test(Arrays.asList("a", "d", "f", "a", "b"));
}
static void test(List<String> setList)
{
List<String> out = setList.stream().sorted((a, b) -> a.compareToIgnoreCase(b)).collect(Collectors.toList());
System.out.println(out);
}
This is properly sorting 1D arrays, so you're correct there.
You'll probably need to implement your own comparator to compare the 2D array lists to sort them.
instead of having the source as a List<String> I'd recommend you have it as a List<Set<String>> e.g.
List<Set<String>> setList = new ArrayList<>();
setList.add(new HashSet<>(Arrays.asList("a","b")));
setList.add(new HashSet<>(Arrays.asList("a","c")));
setList.add(new HashSet<>(Collections.singletonList("a")));
setList.add(new HashSet<>(Collections.singletonList("b")));
setList.add(new HashSet<>());
Then apply the following comparator along with the mapping operation to yield the expected result:
List<String> result =
setList.stream()
.sorted(Comparator.comparing((Function<Set<String>, Boolean>) Set::isEmpty)
.thenComparing(s -> String.join("", s),
String.CASE_INSENSITIVE_ORDER))
.map(Object::toString)
.collect(Collectors.toList());
and this prints:
[[a], [a, b], [a, c], [b], []]
note that, currently the result is a list of strings where each string is the string representation of a given set. if however, you want the result to be a List<Set<String>> then simply remove the map operation above.
Edit:
Managed to get a solution working based on your initial idea....
So, first, you need a completely new comparator instead of just (s1, s2) -> s1.compareToIgnoreCase(s2) as it will not suffice.
Given the input:
Set<String> set = new HashSet<>();
set.add("{}");
set.add("{a}");
set.add("{b}");
set.add("{a, b}");
set.add("{a, c}");
and the following stream pipeline:
List<String> result = set.stream()
.map(s -> s.replaceAll("[^A-Za-z]+", ""))
.sorted(Comparator.comparing(String::isEmpty)
.thenComparing(String.CASE_INSENSITIVE_ORDER))
.map(s -> Arrays.stream(s.split(""))
.collect(Collectors.joining(", ", "{", "}")))
.collect(Collectors.toList());
Then we would have a result of:
[{a}, {a, b}, {a, c}, {b}, {}]
Well, as #Aomine and #Holger noted already, you need a custom comparator.
But IMHO their solutions look over-engineered. You don't need any of costly operations like split and substring:
String.substring creates a new String object and calls System.arraycopy() under the hood
String.split is even more costly. It iterates over your string and calls String.substring multiple times. Moreover it creates an ArrayList to store all the substrings. If the number of substrings is big enough then your ArrayList will need to expand its capacity (perhaps not only once) causing another call of System.arraycopy().
For your simple case I would slightly modify the code of built-in String.compareTo method:
Comparator<String> customComparator =
(s1, s2) -> {
int len1 = s1.length();
int len2 = s2.length();
if (len1 == 2) return 1;
if (len2 == 2) return -1;
int lim = Math.min(len1, len2) - 1;
for (int k = 1; k < lim; k++) {
char c1 = s1.charAt(k);
char c2 = s2.charAt(k);
if (c1 != c2) {
return c1 - c2;
}
}
return len1 - len2;
};
It will compare the strings with complexity O(n), where n is the length of shorter string. At the same time it will neither create any new objects nor perform any array replication.
The same comparator can be implemented using Stream API:
Comparator<String> customComparatorUsingStreams =
(s1, s2) -> {
if (s1.length() == 2) return 1;
if (s2.length() == 2) return -1;
return IntStream.range(1, Math.min(s1.length(), s2.length()) - 1)
.map(i -> s1.charAt(i) - s2.charAt(i))
.filter(i -> i != 0)
.findFirst()
.orElse(0);
};
You can use your custom comparator like this:
List<String> orderedByAlpha = setList.stream()
.sorted(customComparatorUsingStreams)
.collect(Collectors.toList());
System.out.println(orderedByAlpha);
A take on it (slightly similar to the answer by Aomine) would be to strip the strings of the characters that makes String#compareTo() fail, in this case ('{' and '}'). Also, the special case that an empty string ("{}") is to be sorted after the rest needs to be taken care of.
The following code implements such a comparator:
static final Comparator<String> COMPARE_IGNORING_CURLY_BRACES_WITH_EMPTY_LAST = (s1, s2) -> {
Function<String, String> strip = string -> string.replaceAll("[{}]", "");
String strippedS1 = strip.apply(s1);
String strippedS2 = strip.apply(s2);
return strippedS1.isEmpty() || strippedS2.isEmpty() ?
strippedS2.length() - strippedS1.length() :
strippedS1.compareTo(strippedS2);
};
Of course, this is not the most efficient solution. If efficiency is truly important here, I would loop through the characters, like String#compareTo() does, as suggested by ETO.

How to convert a for iteration with conditions to Java 8 stream

Currently, I have this method, which I want to convert to a Java 8 stream style (I have little practice with this API btw, that's the purpose of this little exercise):
private static Map<Integer, List<String>> splitByWords(List<String> list) {
for (int i = 0; i < list.size(); i++) {
if(list.get(i).length() > 30 && list.get(i).contains("-")) {
mapOfElements.put(i, Arrays.stream(list.get(i).split("-")).collect(Collectors.toList()));
} else if(list.get(i).length() > 30) {
mapOfElements.put(i, Arrays.asList(new String[]{list.get(i)}));
} else {
mapOfElements.put(i, Arrays.asList(new String[]{list.get(i) + "|"}));
}
}
return mapOfElements;
}
This is what I´ve got so far:
private static Map<Integer, List<String>> splitByWords(List<String> list) {
Map<Integer, List<String>> mapOfElements = new HashMap<>();
IntStream.range(0, list.size())
.filter(i-> list.get(i).length() > 30 && list.get(i).contains("-"))
.boxed()
.map(i-> mapOfElements.put(i, Arrays.stream(list.get(i).split("-")).collect(Collectors.toList())));
//Copy/paste the above code twice, just changing the filter() and map() functions?
In the "old-fashioned" way, I just need one for iteration to do everything I need regarding my conditions. Is there a way to achieve that using the Stream API or, if I want to stick to it, I have to repeat the above code just changing the filter() and map() conditions, therefore having three for iterations?
The current solution with the for-loop looks good. As you have to distinguish three cases only, there is no need to generalize the processing.
Should there be more cases to distinguish, then it could make sense to refactor the code. My approach would be to explicitly define the different conditions and their corresponding string processing. Let me explain it using the code from the question.
First of all I'm defining the different conditions using an enum.
public enum StringClassification {
CONTAINS_HYPHEN, LENGTH_GT_30, DEFAULT;
public static StringClassification classify(String s) {
if (s.length() > 30 && s.contains("-")) {
return StringClassification.CONTAINS_HYPHEN;
} else if (s.length() > 30) {
return StringClassification.LENGTH_GT_30;
} else {
return StringClassification.DEFAULT;
}
}
}
Using this enum I define the corresponding string processors:
private static final Map<StringClassification, Function<String, List<String>>> PROCESSORS;
static {
PROCESSORS = new EnumMap<>(StringClassification.class);
PROCESSORS.put(StringClassification.CONTAINS_HYPHEN, l -> Arrays.stream(l.split("-")).collect(Collectors.toList()));
PROCESSORS.put(StringClassification.LENGTH_GT_30, l -> Arrays.asList(new String[] { l }));
PROCESSORS.put(StringClassification.DEFAULT, l -> Arrays.asList(new String[] { l + "|" }));
}
Based on this I can do the whole processing using the requested IntStream:
private static Map<Integer, List<String>> splitByWords(List<String> list) {
return IntStream.range(0, list.size()).boxed()
.collect(Collectors.toMap(Function.identity(), i -> PROCESSORS.get(StringClassification.classify(list.get(i))).apply(list.get(i))));
}
The approach is to retrieve for a string the appropriate StringClassification and then in turn the corresponding string processor. The string processors are implementing the strategy pattern by providing a Function<String, List<String>> which maps a String to a List<String> according to the StringClassification.
A quick example:
public static void main(String[] args) {
List<String> list = Arrays.asList("123",
"1-2",
"0987654321098765432109876543211",
"098765432109876543210987654321a-b-c");
System.out.println(splitByWords(list));
}
The output is:
{0=[123|], 1=[1-2|], 2=[0987654321098765432109876543211], 3=[098765432109876543210987654321a, b, c]}
This makes it easy to add or to remove conditions and string processors.
First of I don't see any reason to use the type Map<Integer, List<String>> when the key is an index. Why not use List<List<String>> instead? If you don't use a filter the elements should be on the same index as the input.
The power in a more functional approach is that it's more readable what you're doing. Because you want to do multiple things for multiple sizes strings it's pretty hard write a clean solution. You can however do it in a single loop:
private static List<List<String>> splitByWords(List<String> list)
{
return list.stream()
.map(
string -> string.length() > 30
? Arrays.asList(string.split("-"))
: Arrays.asList(string + "|")
)
.collect(Collectors.toList());
}
You can add more complex logic by making your lambda multiline (not needed in this case). eg.
.map(string -> {
// your complex logic
// don't forget, when using curly braces you'll
// need to return explicitly
return result;
})
The more functional approach would be to group the strings by size followed by applying a specific handler for the different groups. It's pretty hard to keep the index the same, so I change the return value to Map<String, List<String>> so the result can be fetched by providing the original string:
private static Map<String, List<String>> splitByWords(List<String> list)
{
Map<String, List<String>> result = new HashMap<>();
Map<Boolean, List<String>> greaterThan30;
// group elements
greaterThan30 = list.stream().collect(Collectors.groupingBy(
string -> string.length() > 30
));
// handle strings longer than 30 chars
result.putAll(
greaterThan30.get(true).stream().collect(Collectors.toMap(
Function.identity(), // the same as: string -> string
string -> Arrays.asList(string.split("-"))
))
);
// handle strings not longer than 30 chars
result.putAll(
greaterThan30.get(false).stream().collect(Collectors.toMap(
Function.identity(), // the same as: string -> string
string -> Arrays.asList(string + "|")
))
);
return result;
}
The above seems like a lot of hassle, but is in my opinion better understandable. You could also dispatch the logic to handle large and small strings to other methods, knowing the provided string does always match the criteria.
This is slower than the first solution. For a list of size n, it has to loop through n elements to group by the criteria. Then loop through x (0 <= x <= n) elements that match the criteria, followed by a loop through n - x elements that don't match the criteria. (In total 2 times the whole list.)
In this case it might not be worth the trouble since both the criteria, as well as the logic to apply are pretty simple.

Categories