patternMinor
Move a line in a file
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:
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.
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.
Complete (self-contained) example
Here's the entire thing in action:
Disclaimer
These examples were written with Groovy 2.4.4.
-
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.