Skip to content

Commit 11e87d1

Browse files
AngledLuffaclaude
andcommitted
Add RoundTripCoNLLU, which rewrites CoNLL-U files as Ssurgeon writes them
edu.stanford.nlp.trees.ud.RoundTripCoNLLU reads each CoNLL-U file named on the command line and writes it back in place through the CoNLL-U reader and writer, with no edits, the way SsurgeonPattern writes a file. The writer normalizes some things a hand-edited treebank may not have, such as the order of the keys of the Misc column, which it sorts. Ssurgeon writes back every sentence of a file it changes, so its first edit to such a file also changes the Misc column of every sentence, and the edit is lost in the diff. Rewriting the files with this first, and committing that on its own, leaves the Ssurgeon diffs after it with only the changes the edits made. A file already in the writer's form comes back byte for byte the same, as GSD does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016LE4nuNMpegKjF7jZk7Zsb
1 parent eabdd96 commit 11e87d1

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package edu.stanford.nlp.trees.ud;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.nio.charset.StandardCharsets;
6+
import java.nio.file.Files;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
import edu.stanford.nlp.ling.CoreAnnotations;
11+
import edu.stanford.nlp.pipeline.Annotation;
12+
import edu.stanford.nlp.pipeline.CoNLLUReader;
13+
import edu.stanford.nlp.semgraph.SemanticGraph;
14+
import edu.stanford.nlp.semgraph.SemanticGraphCoreAnnotations;
15+
import edu.stanford.nlp.util.CoreMap;
16+
17+
/**
18+
* Rewrites CoNLL-U files through the CoNLL-U reader and writer, with no
19+
* edits, the same way the SsurgeonPattern command line writes a file.
20+
*<br>
21+
* The writer normalizes a few things a hand-edited treebank may not
22+
* have, such as the order of the keys in the Misc column, which it
23+
* sorts. Ssurgeon writes back every sentence of a file it changes,
24+
* so the first Ssurgeon edit to such a file also changes the Misc
25+
* column of every sentence in it, and buries the edit in the diff.
26+
* Rewriting the files with this first, and committing that on its own,
27+
* leaves later Ssurgeon diffs with only the changes the edits made.
28+
*<br>
29+
* Usage:
30+
*<pre>
31+
* java edu.stanford.nlp.trees.ud.RoundTripCoNLLU file.conllu ...
32+
*</pre>
33+
* Each file is rewritten in place. A file which is already in the
34+
* writer's form comes back byte for byte the same.
35+
*
36+
* @author John Bauer
37+
*/
38+
public class RoundTripCoNLLU {
39+
private RoundTripCoNLLU() { }
40+
41+
/**
42+
* Reads a CoNLL-U file and writes it back out as CoNLL-U, with the
43+
* comments, the basic graph, and the enhanced graph of each sentence.
44+
*
45+
* @param path the file to read
46+
* @return the file as the writer writes it
47+
* @throws IOException if the file cannot be read
48+
*/
49+
public static String roundTrip(String path) throws IOException {
50+
CoNLLUReader reader = new CoNLLUReader();
51+
CoNLLUDocumentWriter writer = new CoNLLUDocumentWriter();
52+
StringBuilder result = new StringBuilder();
53+
for (Annotation doc : reader.readCoNLLUFile(path)) {
54+
for (CoreMap sentence : doc.get(CoreAnnotations.SentencesAnnotation.class)) {
55+
SemanticGraph graph = sentence.get(SemanticGraphCoreAnnotations.BasicDependenciesAnnotation.class);
56+
SemanticGraph enhanced = sentence.get(SemanticGraphCoreAnnotations.EnhancedDependenciesAnnotation.class);
57+
List<String> comments = new ArrayList<>();
58+
if (sentence.get(CoreAnnotations.CommentsAnnotation.class) != null) {
59+
comments.addAll(sentence.get(CoreAnnotations.CommentsAnnotation.class));
60+
}
61+
result.append(writer.printSemanticGraph(graph, enhanced, false, comments));
62+
}
63+
}
64+
return result.toString();
65+
}
66+
67+
/**
68+
* Rewrites each file named on the command line in place.
69+
*
70+
* @param args the files to rewrite
71+
* @throws IOException if a file cannot be read or written
72+
*/
73+
public static void main(String[] args) throws IOException {
74+
if (args.length == 0) {
75+
System.err.println("Usage: java " + RoundTripCoNLLU.class.getName() + " file.conllu ...");
76+
System.exit(2);
77+
}
78+
for (String path : args) {
79+
String result = roundTrip(path);
80+
Files.write(new File(path).toPath(), result.getBytes(StandardCharsets.UTF_8));
81+
System.err.println("Rewrote " + path);
82+
}
83+
}
84+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package edu.stanford.nlp.trees.ud;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.nio.charset.StandardCharsets;
6+
import java.nio.file.Files;
7+
8+
import static org.junit.Assert.*;
9+
import org.junit.Test;
10+
11+
public class RoundTripCoNLLUTest {
12+
13+
/** A sentence whose Misc columns are not in the writer's order */
14+
private static final String UNSORTED_MISC = String.join("\n",
15+
"# sent_id = t1",
16+
"# text = Hola mundo.",
17+
"1\tHola\thola\tINTJ\t_\t_\t0\troot\t0:root\tSpaceAfter=No|Gloss=hello",
18+
"2\tmundo\tmundo\tNOUN\t_\tGender=Masc|Number=Sing\t1\tvocative\t1:vocative\tTranslit=mundo|Gloss=world|SpaceAfter=No",
19+
"3\t.\t.\tPUNCT\t_\t_\t1\tpunct\t1:punct\t_",
20+
"", "");
21+
22+
private static File write(String conllu) throws IOException {
23+
File file = File.createTempFile("roundtrip", ".conllu");
24+
file.deleteOnExit();
25+
Files.write(file.toPath(), conllu.getBytes(StandardCharsets.UTF_8));
26+
return file;
27+
}
28+
29+
/**
30+
* The Misc column comes back sorted and nothing else changes, and a
31+
* file written that way comes back the same
32+
*/
33+
@Test
34+
public void testRoundTrip() throws IOException {
35+
File file = write(UNSORTED_MISC);
36+
String expected = UNSORTED_MISC.replace("SpaceAfter=No|Gloss=hello", "Gloss=hello|SpaceAfter=No")
37+
.replace("Translit=mundo|Gloss=world|SpaceAfter=No", "Gloss=world|SpaceAfter=No|Translit=mundo");
38+
assertEquals(expected, RoundTripCoNLLU.roundTrip(file.getPath()));
39+
40+
// main rewrites the file in place, and a second pass changes nothing
41+
RoundTripCoNLLU.main(new String[] { file.getPath() });
42+
assertEquals(expected, new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8));
43+
assertEquals(expected, RoundTripCoNLLU.roundTrip(file.getPath()));
44+
}
45+
}

0 commit comments

Comments
 (0)