11
\$\begingroup\$

I'm used to only processing, at most, one line of a file at a time. This is my first time changing the delimiter, and the objective here is to take a file containing lines such as:

Bubbles,  
Blossom and  
Buttercup

Nostalgic  
Examples for the  
Win.

Quick Brown Fox  
Jumping Over Lazy  
Dog.

and produce single line output:

Bubbles, Blossom and Buttercup.
Nostalgic Examples for the Win.
Quick Brown Fox Jumping Over Lazy Dog.

What I've done works, but it feels like a work-around to some superior alternative I'm sure exists. What do you think?

import java.io.File;
import java.io.FileNotFoundException;
import java.util.regex.Pattern;
import java.util.Scanner;

public class TestDelim {
    public static void main(String[] args) throws FileNotFoundException {
        Scanner input = new Scanner(new File(args[0]))
            .useDelimiter(Pattern.compile("^\\s*$", Pattern.MULTILINE));
        Scanner output;
        StringBuilder sb = new StringBuilder();

        while (input.hasNext()) {
            output = new Scanner(input.next());

            while (output.hasNextLine()) {
                sb.append(' ').append(output.nextLine());
            }

            System.out.println(sb.toString().trim());
            sb.setLength(0);
        }
    }
}
\$\endgroup\$
3
\$\begingroup\$

Multiline text processing takes some getting used to.

First up, if you are going to do multiline processing, then you should read all the data in to a String, and forget about Scanners, etc. 1-line-at-a-time processing is convenient for many reasons, but mostly to reduce the amount of data in memory at any one time. Consider the following:

Path source = Paths.get("poem.txt");
String poem = new String(Files.readAllBytes(source));

Now you have the complete poem in a single variable poem.

Now, a paragraph is identified by an empty line (or more) between texts. In Regex terms, this is two or more newlines and other whitespace:

private static final Pattern PARAGRAPH = Pattern.compile("\\s*^\\s*$\\s*", Pattern.MULTILINE);

Note that \n newline is part of the \\s pattern, so the pattern will match whitespace padded breaks containing at least two newlines.

Also, a pattern for replacing all whitespace with a single space, is:

private static final Pattern MULTISPACE = Pattern.compile("\\s+");

Now, what we need is a compaction routine to convert the input string to a formatted output:

public static String compactLines7(final String source) {
    StringBuilder sb = new StringBuilder(source.length());
    for (String para : PARAGRAPH.split(source)) {
        sb.append(MULTISPACE.matcher(para).replaceAll(" ")).append("\n");
    }
    return sb.toString();
}

Note that the above will leave a trailing newline on the output.

I quite like the Java 8 way, though (which will not have a newline):

public static String compactLines(final String source) {
    return Stream.of(PARAGRAPH.split(source))
            .map(para -> MULTISPACE.matcher(para).replaceAll(" "))
            .collect(Collectors.joining("\n"));
}

Putting this together in code like your example, it is:

private static final Pattern PARAGRAPH = Pattern.compile("\\s*^\\s*$\\s*", Pattern.MULTILINE);
private static final Pattern MULTISPACE = Pattern.compile("\\s+");

public static String compactLines(final String source) {
    return Stream.of(PARAGRAPH.split(source))
            .map(para -> MULTISPACE.matcher(para).replaceAll(" "))
            .collect(Collectors.joining("\n"));
}

public static final void main(String[] args) throws IOException {
    String source = new String(Files.readAllBytes(Paths.get(args[0])));
    System.out.println(compactLines(source));
}
\$\endgroup\$
  • 2
    \$\begingroup\$ I'd love to see you discuss the drawbacks of reading the whole file directly into memory when you propose such a heavy change in the way content is processed. Also these regexes look like you're overly complicating the initial problem :( \$\endgroup\$ – Vogel612 Feb 18 '15 at 23:30
6
\$\begingroup\$

I don't really see why you used two scanners instead of just one. And I also don't see the need for a custom delimiter.

You can use a simpler alternative logic:

  • Process line by line
  • If the line is not empty, append it + a space
  • If the line is empty
    • Print the content buffer, except the last charater (the extra space)
    • Reset the buffer
  • At the end, if the buffer is not empty, print its content, except the last character

That is:

StringBuilder sb = new StringBuilder();

while (input.hasNextLine()) {
    String line = input.nextLine();
    if (!line.isEmpty()) {
        sb.append(line).append(' ');
    } else {
        System.out.println(sb.substring(0, sb.length() - 1));
        sb.setLength(0);
    }
}
if (sb.length() > 0) {
    System.out.println(sb.substring(0, sb.length() - 1));
}

An easy way to "test" this is by instantiating a Scanner with a string containing newlines:

Scanner input = new Scanner("Bubbles,\n" +
        "Blossom and\n" +
        "Buttercup\n" +
        "\n" +
        "Nostalgic\n" +
        "Examples for the\n" +
        "Win.\n" +
        "\n" +
        "Quick Brown Fox\n" +
        "Jumping Over Lazy\n" +
        "Dog.");

I don't know a simpler way to do this. But I have some ideas for possibly interesting variations you could try, for example:

  • Instead of collecting lines in a buffer, print as you go. That will save you memory usage. To track whether to put a space or not, you would need a flag of some sort. (A simple boolean would do.)

  • Instead of printing paragraph by paragraph, let the buffer grow and print at the very end. This uses more memory, but it will make the implementation properly testable, as you could return the buffer, which can be used by unit tests.

Both alternatives have advantages and disadvantages. The simplified version I gave you above is somewhere in the middle.

\$\endgroup\$
5
\$\begingroup\$

You're on the right track with Scanner.useDelimiter(). However, Scanner output is very weird (aren't they always for handling input?), and altogether too complicated for an operation that is essentially a search-and-replace.

public static void main(String[] args) throws FileNotFoundException {
    Scanner input = new Scanner(new File(args[0]))
        .useDelimiter(Pattern.compile("^\\s*$", Pattern.MULTILINE));
    while (input.hasNext()) {
        System.out.println(input.next().trim().replaceAll("\\s+", " "));
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.