Problem
I want to reduce a stream of MatchResults
to a String
. Is there a method in the standard-library for this or do I have to write a custom Collector
?
Currently, my method looks like this:
public static String pointUnderlineRunnables(String line, String regex) {
List<MatchResult> matches = Pattern.compile(regex).matcher(line).results() // Finds matches
.filter(mRes -> ProgramHelper.isRunnableCode(mRes.start(), line)) // Filters out "quoted strings" and comments
.toList();
String underline = "";
for (MatchResult m : matches)
underline = " ".repeat(m.start() - underline.length()) "^".repeat(m.end() - m.start());
return line.strip() "\n" underline;
}
But with the proviced mapping methods I cannot write this:
public static String pointUnderlineRunnables(String line, String regex) {
return line.strip() "\n" Pattern.compile(regex).matcher(line).results() //
.filter(mRes -> ProgramHelper.isRunnableCode(mRes.start(), line)) //
.reduce("", (subtotal, elem) //
-> subtotal " ".repeat(elem.start() - subtotal.length()) "^".repeat(elem.end() - elem.start());
}
...because both the subtotal and the element have to share the same type. Is there another method that I just don't know, that does this?
Edit: The method underlines all occurences of the regex in the line without a few filtered exceptions.
Sample
Input:
- line: "I love food, food is cool!"
- regex: "food"
Output:
I love food, food is cool!
^^^^ ^^^^
CodePudding user response:
You should be able to call this after filter:
.map(mRes -> " ".repeat(m.start() - underline.length()) "^".repeat(m.end() - m.start()).collect(Collectors.joining());
It returns a String
.
So the complete code looks like this:
public static String pointUnderlineRunnables(String line, String regex) {
String underline = Pattern.compile(regex).matcher(line).results() // Finds matches
.filter(mRes -> ProgramHelper.isRunnableCode(mRes.start(), line))
.map(mRes -> " ".repeat(m.start() - underline.length()) "^".repeat(m.end() - m.start())
.collect(Collectors.joining());
return line.strip() "\n" underline;
}
CodePudding user response:
You can use this version of reduce :
public static String pointUnderlineRunnables(String line, String regex) {
String underline = Pattern.compile(regex)
.matcher(line)
.results()
.filter(mRes -> ProgramHelper.isRunnableCode(mRes.start(), line))
.reduce("",
(str,mr) -> str.concat(" ".repeat(mr.start() - str.length()) "^".repeat(mr.end() - mr.start())),
String::concat);
return line.strip() "\n" underline;
}
To make it somehow readable you could extract the BiFunction:
public static String pointUnderlineRunnables(String line, String regex) {
BiFunction<String,MatchResult,String> func = (str,mr) ->
str.concat(" ".repeat(mr.start() - str.length()) "^".repeat(mr.end() - mr.start()));
String underline = Pattern.compile(regex)
.matcher(line)
.results()
.filter(mRes -> ProgramHelper.isRunnableCode(mRes.start(), line))
.reduce("",func::apply, String::concat);
return line.strip() "\n" underline;
}
CodePudding user response:
Collector.of() & StringBuilder are your Friends
You can create a custom collector by using static method Collector.of()
.
We need to provide as arguments:
- Mutable container - in this case an instance of
StringBuilder
having the length equal to the length of the given string, filled with whitespaces, - Accumulator - a function responsible for accumulating the result,
- Combiner - a function that describes how to merge containers holding partial results while executing the stream in parallel
- Finisher - a function (which is optional, all other arguments are mandatory) that performs a final transformation. In this case, turns
StringBuilder
into aString
.
In a nutshell, the idea behind the collector is to create a StringBuilder
filled with whitespaces and having resulting length, and for every MatchResult
replace the chunk in of the StringBuilder
corresponding to its start
and end
with a string comprised of caret symbols ^
.
That's how it can be implemented:
public static String pointUnderlineRunnables(String line, String regex) {
String normalizedLine = line.strip();
return normalizedLine "\n" helper(normalizedLine, regex);
}
public static String helper(String line, String regex) {
return Pattern.compile(regex).matcher(line).results() // Finds matches
.filter(mRes -> ProgramHelper.isRunnableCode(mRes.start(), line)) // Filters out "quoted strings" and comments
.collect(Collector.of(
() -> new StringBuilder(" ".repeat(line.length())),
(StringBuilder res, MatchResult next) ->
res.replace(next.start(), next.end(), "^".repeat(next.end() - next.start())),
(left, right) -> IntStream.range(0, line.length())
.map(i -> Math.max(left.charAt(i), right.charAt(i))) // '^' corresponds to the ASCII index 94 and whitespace to the 32, hence Math.max() will '^' if it's present in either instances of StringBuilder at the given position
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append),
StringBuilder::toString
));
}
main()
public static void main(String[] args) {
System.out.println(pointUnderlineRunnables("I love food, food is cool!", "food"));
}
Output (with filter
operation commented out):
I love food, food is cool!
^^^^ ^^^^