Home > Blockchain >  Is there a method for mapping AND reduction in a stream?
Is there a method for mapping AND reduction in a stream?

Time:07-24

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 a String.

In a nutshell, the idea behind the collector is to create a StringBuilderfilled 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!
       ^^^^  ^^^^         

A link to Online Demo

  • Related