Skip to content

Commit ae74790

Browse files
committed
OPENNLP-1930: Read AD whitespace by the Unicode definition and tidy the scans
The tree and markup scans, the guillemet fix, the POS tag join, the contraction split, and the NER tag split use the Unicode White_Space property through StringUtil.isUnicodeWhitespace and splitOnUnicodeWhitespace, so the samples no longer depend on the whitespace mode or on the shared WhitespaceTokenizer instance. The scans return records instead of arrays, the level prefix and the next closing parenthesis are computed once per line, the memo of the leaf scan is allocated only when a quoted lemma follows a tag, and the character literals of the grammar are constants. A fallback line with no text after its last equals sign is skipped, an id that does not fit into an int is invalid metadata, a contraction lexeme of underscores only has no left part, and the helpers are instance methods. The manual states the whitespace definition, the id rules, and the self-closing tags.
1 parent eca9b25 commit ae74790

11 files changed

Lines changed: 500 additions & 428 deletions

File tree

opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/ad/ADMetadata.java

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,36 +27,45 @@ final class ADMetadata {
2727

2828
private static final String PARAGRAPH_PREFIX = "p=";
2929
private static final String SOURCE_PREFIX = "source=\"";
30+
private static final char HYPHEN = '-';
31+
private static final char QUOTE = '"';
3032

31-
private ADMetadata() {
33+
/**
34+
* The ids of a sentence.
35+
*
36+
* @param text The text id.
37+
* @param paragraph The paragraph id.
38+
*/
39+
record TextAndParagraph(int text, int paragraph) {
3240
}
3341

3442
/**
35-
* Parses the text id and the paragraph id: the text id is the ASCII digit run after any
36-
* leading ASCII letters and hyphens, the paragraph id the digit run after the first
37-
* {@code p=} that at least one digit follows.
38-
*
39-
* @param meta The metadata.
40-
* @return The text id and the paragraph id, or {@code null} if either is missing.
43+
* Where the digit runs of the two ids lie in the metadata, each as an inclusive start and
44+
* an exclusive end.
4145
*/
42-
static int[] parseTextAndParagraph(String meta) {
43-
int[] spans = scanTextAndParagraph(meta);
44-
if (spans == null) {
45-
return null;
46-
}
47-
return new int[] {Integer.parseInt(meta.substring(spans[0], spans[1])),
48-
Integer.parseInt(meta.substring(spans[2], spans[3]))};
46+
private record IdSpans(int textStart, int textEnd, int paragraphStart, int paragraphEnd) {
47+
}
48+
49+
private ADMetadata() {
4950
}
5051

5152
/**
52-
* Reads the digits of the text id, see {@link #parseTextAndParagraph(String)}.
53+
* Parses the text id and the paragraph id: the text id is the ASCII digit run that directly
54+
* follows the leading ASCII letters and hyphens, the paragraph id the digit run after the
55+
* first {@code p=} that at least one digit follows.
5356
*
5457
* @param meta The metadata.
55-
* @return The digits, or {@code null} if the text id or the paragraph id is missing.
58+
* @return The two ids, or {@code null} if either is missing or does not fit into an
59+
* {@code int}.
5660
*/
57-
static String textId(String meta) {
58-
int[] spans = scanTextAndParagraph(meta);
59-
return spans == null ? null : meta.substring(spans[0], spans[1]);
61+
static TextAndParagraph parseTextAndParagraph(String meta) {
62+
IdSpans spans = scanTextAndParagraph(meta);
63+
if (spans == null) {
64+
return null;
65+
}
66+
int text = parseDigits(meta, spans.textStart(), spans.textEnd());
67+
int paragraph = parseDigits(meta, spans.paragraphStart(), spans.paragraphEnd());
68+
return text == -1 || paragraph == -1 ? null : new TextAndParagraph(text, paragraph);
6069
}
6170

6271
/**
@@ -68,8 +77,8 @@ static String textId(String meta) {
6877
* missing.
6978
*/
7079
static String textPrefix(String meta) {
71-
int[] spans = scanTextAndParagraph(meta);
72-
return spans == null || spans[0] == 0 ? null : meta.substring(0, spans[0]);
80+
IdSpans spans = scanTextAndParagraph(meta);
81+
return spans == null || spans.textStart() == 0 ? null : meta.substring(0, spans.textStart());
7382
}
7483

7584
/**
@@ -84,28 +93,26 @@ static String source(String meta) {
8493
return null;
8594
}
8695
start += SOURCE_PREFIX.length();
87-
int end = meta.indexOf('"', start);
96+
int end = meta.indexOf(QUOTE, start);
8897
return end == -1 ? null : meta.substring(start, end);
8998
}
9099

91100
/**
92101
* Scans the text id and the paragraph id, see {@link #parseTextAndParagraph(String)}.
93102
*
94103
* @param meta The metadata.
95-
* @return The start and end of the text id and the start and end of the paragraph id, or
96-
* {@code null} if either is missing.
104+
* @return The spans of the two ids, or {@code null} if either is missing.
97105
*/
98-
private static int[] scanTextAndParagraph(String meta) {
106+
private static IdSpans scanTextAndParagraph(String meta) {
99107
int i = 0;
100-
while (i < meta.length() && (StringUtil.isAsciiLetter(meta.charAt(i)) || meta.charAt(i) == '-')) {
108+
while (i < meta.length() && (StringUtil.isAsciiLetter(meta.charAt(i)) || meta.charAt(i) == HYPHEN)) {
101109
i++;
102110
}
103111
int textStart = i;
104-
i = StringUtil.endOfAsciiDigits(meta, i);
105-
if (i == textStart) {
112+
int textEnd = StringUtil.endOfAsciiDigits(meta, i);
113+
if (textEnd == textStart) {
106114
return null;
107115
}
108-
int textEnd = i;
109116
int from = textEnd;
110117
while (true) {
111118
int prefix = meta.indexOf(PARAGRAPH_PREFIX, from);
@@ -115,10 +122,30 @@ private static int[] scanTextAndParagraph(String meta) {
115122
int paragraphStart = prefix + PARAGRAPH_PREFIX.length();
116123
int paragraphEnd = StringUtil.endOfAsciiDigits(meta, paragraphStart);
117124
if (paragraphEnd > paragraphStart) {
118-
return new int[] {textStart, textEnd, paragraphStart, paragraphEnd};
125+
return new IdSpans(textStart, textEnd, paragraphStart, paragraphEnd);
119126
}
120127
from = prefix + 1;
121128
}
122129
}
123130

131+
/**
132+
* Reads a run of ASCII digits as a number.
133+
*
134+
* @param meta The metadata.
135+
* @param start The inclusive start of the run.
136+
* @param end The exclusive end of the run.
137+
* @return The number, or -1 if it does not fit into an {@code int}.
138+
*/
139+
private static int parseDigits(String meta, int start, int end) {
140+
int value = 0;
141+
for (int i = start; i < end; i++) {
142+
int digit = meta.charAt(i) - '0';
143+
if (value > (Integer.MAX_VALUE - digit) / 10) {
144+
return -1;
145+
}
146+
value = value * 10 + digit;
147+
}
148+
return value;
149+
}
150+
124151
}

opennlp-core/opennlp-formats/src/main/java/opennlp/tools/formats/ad/ADNameSampleStream.java

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@
3232
import opennlp.tools.formats.ad.ADSentenceStream.SentenceParser.Node;
3333
import opennlp.tools.formats.ad.ADSentenceStream.SentenceParser.TreeElement;
3434
import opennlp.tools.namefind.NameSample;
35-
import opennlp.tools.tokenize.WhitespaceTokenizer;
3635
import opennlp.tools.util.InputStreamFactory;
3736
import opennlp.tools.util.ObjectStream;
3837
import opennlp.tools.util.PlainTextByLineStream;
3938
import opennlp.tools.util.Span;
39+
import opennlp.tools.util.StringUtil;
4040

4141
/**
4242
* Parser for Floresta Sita(c)tica Arvores Deitadas corpus, output to for the
@@ -59,6 +59,9 @@
5959
* Detailed info about the
6060
* <a href="http://beta.visl.sdu.dk/visl/pt/info/portsymbol.html#semtags_names">NER tagset</a>.
6161
* <p>
62+
* Whitespace inside the tags of a leaf and in a contraction is the Unicode White_Space property,
63+
* see {@link StringUtil#isUnicodeWhitespace(char)}, independent of the whitespace mode.
64+
* <p>
6265
* <b>Note:</b>
6366
* Do not use this class, internal use only!
6467
*/
@@ -70,6 +73,16 @@ public class ADNameSampleStream implements ObjectStream<NameSample> {
7073
*/
7174
private static final Map<String, String> HAREM;
7275

76+
private static final String NER_PREFIX = "NER:";
77+
private static final String HYPHEN = "-";
78+
private static final char HYPHEN_CHAR = '-';
79+
private static final char UNDERSCORE = '_';
80+
private static final char TAG_OPEN = '<';
81+
private static final char TAG_CLOSE = '>';
82+
private static final String LITERARY_PREFIX = "LIT";
83+
private static final String SCIENTIFIC_PREFIX = "CIE";
84+
private static final String INVALID_METADATA = "Invalid metadata: ";
85+
7386
static {
7487
Map<String, String> harem = new HashMap<>();
7588

@@ -243,7 +256,7 @@ private void processLeaf(Leaf leaf, List<String> sentence, List<Span> names) {
243256
String c = PortugueseContractionUtility.toContraction(
244257
leftContractionPart, right);
245258
if (c != null) {
246-
String[] parts = WhitespaceTokenizer.INSTANCE.tokenize(c);
259+
String[] parts = StringUtil.splitOnUnicodeWhitespace(c);
247260
sentence.addAll(Arrays.asList(parts));
248261
alreadyAdded = true;
249262
} else {
@@ -266,7 +279,7 @@ private void processLeaf(Leaf leaf, List<String> sentence, List<Span> names) {
266279
if (lexemes.length > 1) {
267280
sentence.addAll(Arrays.asList(lexemes).subList(0, lexemes.length - 1));
268281
}
269-
leftContractionPart = lexemes[lexemes.length - 1];
282+
leftContractionPart = lexemes.length == 0 ? null : lexemes[lexemes.length - 1];
270283
return;
271284
}
272285
if (leafTag.contains("<NER2>")) {
@@ -335,12 +348,12 @@ private List<String> processTok(String tok) {
335348
}
336349

337350
// lets split all hyphens
338-
if (this.splitHyphenatedTokens && tok.contains("-") && tok.length() > 1) {
351+
if (this.splitHyphenatedTokens && tok.contains(HYPHEN) && tok.length() > 1) {
339352
String[] parts = matchHyphenatedToken(tok);
340353

341354
if (parts != null) {
342355
addIfNotEmpty(parts[0], out);
343-
addIfNotEmpty("-", out);
356+
addIfNotEmpty(HYPHEN, out);
344357
addIfNotEmpty(parts[1], out);
345358
addIfNotEmpty(parts[2], out);
346359
tokAdded = true;
@@ -372,11 +385,17 @@ private void addIfNotEmpty(String firstTok, List<String> out) {
372385
* @param s The lexeme.
373386
* @return The parts in order; empty when the lexeme has no character other than underscores.
374387
*/
375-
static String[] splitOnUnderscores(String s) {
388+
String[] splitOnUnderscores(String s) {
389+
if (s.isEmpty()) {
390+
return new String[0];
391+
}
392+
if (s.indexOf(UNDERSCORE) == -1) {
393+
return new String[] {s};
394+
}
376395
List<String> tokens = new ArrayList<>();
377396
int start = -1;
378397
for (int i = 0; i < s.length(); i++) {
379-
if (s.charAt(i) == '_') {
398+
if (s.charAt(i) == UNDERSCORE) {
380399
if (start >= 0) {
381400
tokens.add(s.substring(start, i));
382401
start = -1;
@@ -398,7 +417,7 @@ static String[] splitOnUnderscores(String s) {
398417
* @return {@code true} if the token is non-empty and every code point is a letter or a
399418
* decimal digit.
400419
*/
401-
static boolean isAlphaNumeric(String tok) {
420+
boolean isAlphaNumeric(String tok) {
402421
if (tok.isEmpty()) {
403422
return false;
404423
}
@@ -422,23 +441,20 @@ static boolean isAlphaNumeric(String tok) {
422441
* @return The first token, second token, and rest, each {@code null} when absent, or
423442
* {@code null} if the token has none of the three shapes.
424443
*/
425-
static String[] matchHyphenatedToken(String tok) {
444+
String[] matchHyphenatedToken(String tok) {
426445
int len = tok.length();
427-
// (\p{L}+)-$
428-
if (len > 1 && tok.charAt(len - 1) == '-' && isAllLetters(tok, 0, len - 1)) {
446+
if (len > 1 && tok.charAt(len - 1) == HYPHEN_CHAR && lettersEnd(tok, 0) == len - 1) {
429447
return new String[] {tok.substring(0, len - 1), null, null};
430448
}
431-
// ^-(\p{L}+)(.*)
432-
if (tok.charAt(0) == '-') {
449+
if (tok.charAt(0) == HYPHEN_CHAR) {
433450
int lettersEnd = lettersEnd(tok, 1);
434451
if (lettersEnd > 1) {
435452
return new String[] {null, tok.substring(1, lettersEnd), tok.substring(lettersEnd)};
436453
}
437454
return null;
438455
}
439-
// (\p{L}+)-(\p{L}+)(.*)
440456
int firstEnd = lettersEnd(tok, 0);
441-
if (firstEnd > 0 && firstEnd + 1 < len && tok.charAt(firstEnd) == '-') {
457+
if (firstEnd > 0 && firstEnd + 1 < len && tok.charAt(firstEnd) == HYPHEN_CHAR) {
442458
int secondEnd = lettersEnd(tok, firstEnd + 1);
443459
if (secondEnd > firstEnd + 1) {
444460
return new String[] {tok.substring(0, firstEnd),
@@ -455,7 +471,7 @@ static String[] matchHyphenatedToken(String tok) {
455471
* @param from The start offset.
456472
* @return The offset after the run, or {@code from} if no letter starts there.
457473
*/
458-
private static int lettersEnd(String s, int from) {
474+
private int lettersEnd(String s, int from) {
459475
int i = from;
460476
while (i < s.length()) {
461477
int cp = s.codePointAt(i);
@@ -467,38 +483,18 @@ private static int lettersEnd(String s, int from) {
467483
return i;
468484
}
469485

470-
/**
471-
* Tests whether a range holds letters only.
472-
*
473-
* @param s The text.
474-
* @param from The inclusive start.
475-
* @param to The exclusive end.
476-
* @return {@code true} if every code point in the range is a letter.
477-
*/
478-
private static boolean isAllLetters(String s, int from, int to) {
479-
int i = from;
480-
while (i < to) {
481-
int cp = s.codePointAt(i);
482-
if (!Character.isLetter(cp)) {
483-
return false;
484-
}
485-
i += Character.charCount(cp);
486-
}
487-
return true;
488-
}
489-
490486
/**
491487
* Extracts the content of a NER tag in Arvores Deitadas format, between the optional
492488
* {@code NER:} prefix and the closing angle bracket.
493489
*
494490
* @param t The tag.
495491
* @return The content, or {@code null} if {@code t} is not enclosed in angle brackets.
496492
*/
497-
static String tagContent(String t) {
498-
if (t.length() < 2 || t.charAt(0) != '<' || t.charAt(t.length() - 1) != '>') {
493+
String tagContent(String t) {
494+
if (t.length() < 2 || t.charAt(0) != TAG_OPEN || t.charAt(t.length() - 1) != TAG_CLOSE) {
499495
return null;
500496
}
501-
int start = t.startsWith("NER:", 1) ? 5 : 1;
497+
int start = t.startsWith(NER_PREFIX, 1) ? 1 + NER_PREFIX.length() : 1;
502498
return t.substring(start, t.length() - 1);
503499
}
504500

@@ -508,11 +504,11 @@ static String tagContent(String t) {
508504
* @param tags The NER tag in Arvores Deitadas format.
509505
* @return The NER tag, or {@code null} if not a NER tag in Arvores Deitadas format.
510506
*/
511-
private static String getNER(String tags) {
507+
private String getNER(String tags) {
512508
if (tags.contains("<NER2>")) {
513509
return null;
514510
}
515-
String[] tag = WhitespaceTokenizer.INSTANCE.tokenize(tags);
511+
String[] tag = StringUtil.splitOnUnicodeWhitespace(tags);
516512
for (String t : tag) {
517513
String ner = tagContent(t);
518514
if (ner != null && HAREM.containsKey(ner)) {
@@ -532,29 +528,31 @@ public void close() throws IOException {
532528
adSentenceStream.close();
533529
}
534530

531+
/**
532+
* Reads the id of the text a sentence belongs to; adaptive data is cleared when it changes. In
533+
* the Amazonia corpus it is the text id of the metadata. In the literary and scientific corpora
534+
* the text name stands in for it, and the id is the same for all sentences (OPENNLP-1951).
535+
*
536+
* @param paragraph The sentence.
537+
* @return The id.
538+
* @throws RuntimeException If the metadata has no id or one that does not fit into an
539+
* {@code int}.
540+
*/
535541
private int getTextID(Sentence paragraph) {
536-
537542
final String meta = paragraph.metadata();
538-
int textIdMeta2 = -1;
539-
String textMeta2 = "";
540-
541-
if (meta.startsWith("LIT") || meta.startsWith("CIE")) {
542-
String textId = meta.startsWith("LIT") ? ADMetadata.textPrefix(meta) : ADMetadata.source(meta);
543-
if (textId == null) {
544-
throw new RuntimeException("Invalid metadata: " + meta);
545-
}
546-
if (!textId.equals(textMeta2)) {
547-
textIdMeta2++;
548-
textMeta2 = textId;
543+
boolean literary = meta.startsWith(LITERARY_PREFIX);
544+
if (literary || meta.startsWith(SCIENTIFIC_PREFIX)) {
545+
String textName = literary ? ADMetadata.textPrefix(meta) : ADMetadata.source(meta);
546+
if (textName == null) {
547+
throw new RuntimeException(INVALID_METADATA + meta);
549548
}
550-
return textIdMeta2;
549+
return textName.isEmpty() ? -1 : 0;
551550
}
552-
// Amazonia
553-
String textId = ADMetadata.textId(meta);
554-
if (textId == null) {
555-
throw new RuntimeException("Invalid metadata: " + meta);
551+
ADMetadata.TextAndParagraph ids = ADMetadata.parseTextAndParagraph(meta);
552+
if (ids == null) {
553+
throw new RuntimeException(INVALID_METADATA + meta);
556554
}
557-
return Integer.parseInt(textId);
555+
return ids.text();
558556
}
559557

560558
}

0 commit comments

Comments
 (0)