0

I get some result from an external command (semi-api) and want to parse the result. I'm only interested in the last few lines of the result.

How can get the last x lines of a string in Java?

AlikElzin-kilaka
  • 34,335
  • 35
  • 194
  • 277

5 Answers5

3

Here's a simple solution:

public static List<String> getLastLines(String string, int numLines) {
    List<String> lines = Arrays.asList(string.split("\n"));
    return new ArrayList<>(lines.subList(Math.max(0, lines.size() - numLines), lines.size()));
}
shmosel
  • 49,289
  • 6
  • 73
  • 138
  • Was about to post the same. – Seth Mar 31 '17 at 06:51
  • Using `System.lineSeparator()` instead of `"\n"` might be better, given that the string is generated by the same system. – walen Mar 31 '17 at 06:55
  • @walen How do you know where the string is generated? I was thinking of using [this](http://stackoverflow.com/a/3445417/1553851) but didn't want to complicate things. – shmosel Mar 31 '17 at 06:56
  • I understood "external _command_" to be a system command external to the Java program but internal to the system. If it said "external _call_" that'd be a different story. I guess different people may use different terminology, though. – walen Mar 31 '17 at 07:01
  • 2
    splitting up the string like this seems like a bad idea if the string is huge and numLines is relatively small? – muzzlator Mar 31 '17 at 07:01
  • Oh, sure. I suppose their question should say they want it performant if that was a constraint :) – muzzlator Mar 31 '17 at 07:03
  • Won't work for consecutive `\n`, e,g, `\n\ntext1\n\ntext2\n\n`. – aUserHimself Mar 31 '17 at 07:04
  • @aUserHimself why not? – muzzlator Mar 31 '17 at 07:06
  • @muzzlator Because you will get empty `Strings` in between `\n\n`. – aUserHimself Mar 31 '17 at 07:07
  • @shmosel Ok then, it depends on how you interpret it. So if my text contains only three new lines, then in fact I can return the last 2 elements of my text: 2 empty `Strings`. – aUserHimself Mar 31 '17 at 07:08
1

A solution that gives the result without parsing the whole string:

/**
 * Created by alik on 3/31/17.
 */
public class Main {

    // TODO: Support other EndOfLines, like "\r\n".
    // One way is to just replace all "\r\n" with "\n" and then run the @getLastLines method.
    public static List<String> getLastLines(String string, int numLines) {
        List<String> lines = new ArrayList<>();
        int currentEndOfLine = string.length();
        if (string.endsWith("\n")) {
            currentEndOfLine = currentEndOfLine - "\n".length();
        }
        for (int i = 0; i < numLines; ++i) {
            int lastEndOfLine = currentEndOfLine;
            currentEndOfLine = string.lastIndexOf("\n", lastEndOfLine - 1);
            String lastLine = string.substring(currentEndOfLine + 1, lastEndOfLine);
            lines.add(0, lastLine);
        }
        return lines;
    }

    @Test
    public void test1() {
        String text = "111\n" +
                "222\n" +
                "333\n" +
                "444\n" +
                "555\n" +
                "666\n" +
                "777\n";
        List<String> lastLines = getLastLines(text, 4);
        Assert.assertEquals("777", lastLines.get(lastLines.size() - 1));
        Assert.assertEquals(4, lastLines.size());
    }

    @Test
    public void test2() {
        String text = "111\n" +
                "222\n" +
                "333\n" +
                "444\n" +
                "555\n" +
                "666\n" +
                "777";
        List<String> lastLines = getLastLines(text, 4);
        Assert.assertEquals("777", lastLines.get(lastLines.size() - 1));
        Assert.assertEquals(4, lastLines.size());
    }
}

* Link to github gist

AlikElzin-kilaka
  • 34,335
  • 35
  • 194
  • 277
  • One simple speed-up would be to change `lines.add(0, lastLine)` to just `lines.add(lastLine)` and reverse it later (Using`Iterables#reverse` would make it concise) – muzzlator Mar 31 '17 at 06:45
  • @muzzlator What's `Iterables#reverse`? – shmosel Mar 31 '17 at 06:47
  • @shmosel Sorry I was thinking of `Lists#reverse` in guava. [Docs](https://google.github.io/guava/releases/snapshot/api/docs/com/google/common/collect/Lists.html#reverse-java.util.List-) – muzzlator Mar 31 '17 at 06:57
0

Algorithm

  • Split input text with line break character and save as a list lines
  • If number of lines required, linesRequired is less than size of lines, ie, lineCount
    • Then return sublist of lines starting from lineCount - linesRequired to lineCount.
    • Otherwise, throw exception or return all lines based on the requrement.

Sample Implementation

private static final String SEPARATOR = "\n";

public static List<String> getLastLines(String string, int numLines) {
    List<String> lines = Arrays.asList(string.split(SEPARATOR));
    int lineCount = lines.size();
    return lineCount > numLines ? lines.subList(lineCount - numLines, lineCount) : lines;
}
niyasc
  • 4,440
  • 1
  • 23
  • 50
0

Here is another possible solution:

public class LastNLines {
    public static List<String> getLastNLines(String inputString, int n) {
        if(n < 0) {
            return new ArrayList<>();
        }
        String[] tmp = inputString.split("(\\n|\\r)+");
        if(n < tmp.length) {
            return Arrays.asList(Arrays.copyOfRange(tmp, tmp.length - n, tmp.length));
        }
        return Arrays.asList(tmp);
    }

    public static void main(String[] args) {
         String myExample =
            "  \n\r\r\n\nsome_text_1  " +
            "\n" +
            "some_text_2\n\n" +
            "\n" +
            "    some_text_3\n\r    " +
            "\n" +
            "some_text_4\n" +
            "\n" +
            "some_text_5\n\n\n";
        List<String> result = LastNLines.getLastNLines(myExample, 2);
        System.out.println(result.toString());
    }
}

This one splits by multiple new lines at once, so a text like this \n\n\n\n\n will contain no Strings after splitting and the result will be empty.

aUserHimself
  • 1,589
  • 2
  • 17
  • 26
0

Java 8 Streams (memory friendly) answer.

Code:

public class Main
{
    public static void main( String[] args )
    {
        String rawData = "John\n\nDavid\nGeorge\nFrank\nTom";

        Pattern pattern = Pattern.compile("\\n");  

        System.out.println( lastN( pattern.splitAsStream( rawData ),4));
        System.out.println( lastN( pattern.splitAsStream( rawData ),40));
    }

    public static <T> List<T> lastN( Stream<T> stream, int n )
    {
        Deque<T> result = new ArrayDeque<>( n );
        stream.forEachOrdered( x -> {
            if ( result.size() == n )
            {
                result.pop();
            }
            result.add( x );
        } );
        return new ArrayList<>( result );
    }
}
Rob Audenaerde
  • 19,195
  • 10
  • 76
  • 121
  • That's a completely pointless use of streams. You could just as well use `Arrays.asList(...).forEach(...)`. – shmosel Mar 31 '17 at 06:59
  • Still kind of weak. You're iterating over the entire sequence, so there's no real advantage to using `splitAsStream()`. – shmosel Mar 31 '17 at 07:13
  • OP asked starting with a `String`. My answer can be adapted for `InputStream` really easily (by wrapping `BufferedReader.lines()` which also returns a `Stream`. This is memory efficient. – Rob Audenaerde Mar 31 '17 at 07:16
  • Fair point. But you could accomplish the same by accepting an `Iterator` and calling `forEachRemaining()`. – shmosel Mar 31 '17 at 07:20
  • 1
    Sure, there are more roads that lead to Rome :) – Rob Audenaerde Mar 31 '17 at 07:22