HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Move a line in a file

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
movefileline

Problem

In Groovy 1.8.6 I need to move a line just before another one. I'm interested in any improvements.

fileToChange = new File('.classpath')
linesToKeep = []
fileToChange.eachLine {
if (it.contains('line to move')) {
lineToMove = it
} else {
linesToKeep.add(it)
}
}
fileToChange.withWriter { out ->
linesToKeep.each {
if (it.contains('line to recognize')) {
out.writeLine(lineToMove)
}
out.writeLine(it)
}
}

Solution

As you figured out, moving a line requires two passes:

-
one to determine what to move and where to move it to,

-
and another to re-write the file with the lines moved.

With some meta-programming you can add a method to the InputStream class that would allow you to move a line ahead of another using concise code like this:

def source = /line to move/
def dest = /line to recognize/

input.moveLine(source, dest).writeTo(writer)


The meta-programming

You can start with a Groovy Category that adds the method Writable moveLine(Object, Object) to a class, such as an InputStream.

@groovy.lang.Category(InputStream)
class MoveLineCategory {

    Writable moveLine(Object sourceMatcher, Object destinationMatcher) {
        def (sourceLine, lines) = this.readLines().inject([null, []]) {output, line ->
            switch(line) {
                case ~sourceMatcher:
                    if(!output[0]) output[0] = line
                    break
                case ~destinationMatcher:
                    output[1] 
            lines.each {line ->
                if(line == null) writer << "$sourceLine\n"
                else writer << "$line\n"
            }
        }.asWritable()
    }
}


The first pass is similar to your File.eachLine() loop except it's done with Collection.inject(). The inject() method Closure returns a List containing two items: the line which matched the sourceMatcher regular expression, and a List of the entire file with a null inserted as a place holder for the destination of the matched line.

The second pass is actually not executed right way. Instead, a Closure coerced into a Writable is returned for later use. The returned Writable contains the code needed to write out the changed file.

Using InputStream.moveLine()

With the Writable on hand, it's now possible to write the file with the line moved. But to even use the InputStream.moveLine() method the Category in which it's defined needs to be made available. the Object.use(Class) method does this.

def source = /line to move/
def dest = /line to recognize/

use(MoveLineCategory) {
    input.moveLine(source, dest).writeTo(writer) 
}


Complete (self-contained) example

Here's the entire thing in action:

@groovy.lang.Category(InputStream)
class MoveLineCategory {

    Writable moveLine(Object sourceMatcher, Object destinationMatcher) {
        def (sourceLine, lines) = this.readLines().inject([null, []]) {output, line ->
            switch(line) {
                case ~sourceMatcher:
                    if(!output[0]) output[0] = line
                    break
                case ~destinationMatcher:
                    output[1] 
            lines.each {line ->
                if(line == null) writer << "$sourceLine\n"
                else writer << "$line\n"
            }
        }.asWritable()
    }
}

def input = ['''This is a line.
And this is another line.
And this is yet another line.
You guessed it, another line.'''] as StringBufferInputStream

def writer = new StringWriter()

use(MoveLineCategory) {
    input
        .moveLine(/And this is another line\./, /You guessed it, another line\./)
        .writeTo(writer)     
}

assert writer.toString().split(/\n/) == [
    'This is a line.',
    'And this is yet another line.',
    'And this is another line.',
    'You guessed it, another line.'
]


Disclaimer

These examples were written with Groovy 2.4.4.

Code Snippets

def source = /line to move/
def dest = /line to recognize/

input.moveLine(source, dest).writeTo(writer)
@groovy.lang.Category(InputStream)
class MoveLineCategory {

    Writable moveLine(Object sourceMatcher, Object destinationMatcher) {
        def (sourceLine, lines) = this.readLines().inject([null, []]) {output, line ->
            switch(line) {
                case ~sourceMatcher:
                    if(!output[0]) output[0] = line
                    break
                case ~destinationMatcher:
                    output[1] << null
                    output[1] << line
                    break;
                default:
                    output[1] << line
            }

            return output
        }

        {Writer writer ->
            lines.each {line ->
                if(line == null) writer << "$sourceLine\n"
                else writer << "$line\n"
            }
        }.asWritable()
    }
}
def source = /line to move/
def dest = /line to recognize/

use(MoveLineCategory) {
    input.moveLine(source, dest).writeTo(writer) 
}
@groovy.lang.Category(InputStream)
class MoveLineCategory {

    Writable moveLine(Object sourceMatcher, Object destinationMatcher) {
        def (sourceLine, lines) = this.readLines().inject([null, []]) {output, line ->
            switch(line) {
                case ~sourceMatcher:
                    if(!output[0]) output[0] = line
                    break
                case ~destinationMatcher:
                    output[1] << null
                    output[1] << line
                    break;
                default:
                    output[1] << line
            }

            return output
        }

        {Writer writer ->
            lines.each {line ->
                if(line == null) writer << "$sourceLine\n"
                else writer << "$line\n"
            }
        }.asWritable()
    }
}

def input = ['''This is a line.
And this is another line.
And this is yet another line.
You guessed it, another line.'''] as StringBufferInputStream

def writer = new StringWriter()

use(MoveLineCategory) {
    input
        .moveLine(/And this is another line\./, /You guessed it, another line\./)
        .writeTo(writer)     
}

assert writer.toString().split(/\n/) == [
    'This is a line.',
    'And this is yet another line.',
    'And this is another line.',
    'You guessed it, another line.'
]

Context

StackExchange Code Review Q#75522, answer score: 2

Revisions (0)

No revisions yet.