Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0144431
OPENNLP-1928: Replace cap and digit patterns with ASCII range checks …
krickert Sep 5, 2026
71a79d9
OPENNLP-1928: Replace cap and digit patterns with ASCII range checks …
krickert Sep 5, 2026
b4c44c0
OPENNLP-1928: Replace capPeriod pattern with explicit length and char…
krickert Sep 5, 2026
c0a731b
OPENNLP-1928: Replace noLetters pattern with a char range scan in Tok…
krickert Sep 5, 2026
ad7142a
OPENNLP-1928: Replace typed outcome pattern with lastIndexOf and word…
krickert Sep 5, 2026
44d6b34
OPENNLP-1928: Replace tab pattern split with explicit tab splitting i…
krickert Sep 5, 2026
1e466bd
OPENNLP-1928: Replace whitespace split and alphanumeric pattern with …
krickert Sep 5, 2026
e2c1d70
OPENNLP-1928: Replace emoji pattern with a code point range scan in E…
krickert Sep 5, 2026
32b9bee
OPENNLP-1928: Replace token id split and text language pattern with e…
krickert Sep 5, 2026
99891dc
OPENNLP-1928: Replace untokenized paren patterns with explicit two-pa…
krickert Sep 5, 2026
426e49a
OPENNLP-1928: Replace checksum split and link pattern with explicit s…
krickert Sep 5, 2026
09e531c
OPENNLP-1928: Replace tag, whitespace, underline, hyphen, and alphanu…
krickert Sep 5, 2026
80e448f
OPENNLP-1928: Replace whitespace pattern with an explicit scan in ADP…
krickert Sep 5, 2026
26950fd
OPENNLP-1928: Replace metadata pattern with an explicit scan in ADSen…
krickert Sep 5, 2026
ca15512
OPENNLP-1928: Replace punctuation patterns with explicit scans in ADS…
krickert Sep 5, 2026
2b8db9e
OPENNLP-1928: Document the scan helpers that replace the patterns
krickert Sep 7, 2026
85dceea
OPENNLP-1928: Share the ASCII scan helpers and test each helper
krickert Sep 7, 2026
ea469d4
OPENNLP-1930: Scan the AD markup tags in ADSentenceStream
krickert Sep 7, 2026
fc535d8
OPENNLP-1930: Scan the AD node and leaf lines in SentenceParser
krickert Sep 7, 2026
f2fbb71
OPENNLP-1930: Share the AD metadata scan between the sample streams
krickert Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions opennlp-api/src/main/java/opennlp/tools/util/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ public static boolean isUnicodeWhitespace(int charCode) {
return UnicodeWhitespace.isWhitespace(charCode);
}

/**
* Determines if the specified {@link Character} is one of the six ASCII whitespace
* characters, the set the regular expression class {@code \s} matches: space, tab,
* line feed, vertical tab, form feed, and carriage return. Unlike
* {@link #isWhitespace(char)}, this ignores the {@link WhitespaceMode} and rejects
* every non-ASCII space.
*
* @param charCode The character to check.
*
* @return {@code true} if {@code charCode} is one of those six characters,
* {@code false} otherwise.
*/
public static boolean isAsciiWhitespace(char charCode) {
return charCode == ' ' || charCode == '\t' || charCode == '\n' || charCode == '\u000B'
|| charCode == '\f' || charCode == '\r';
}

/**
* Splits {@code input} on runs of Unicode {@code White_Space}. Leading and trailing
* runs are ignored, so whitespace-only input yields an empty array. This is a
Expand Down Expand Up @@ -151,6 +168,91 @@ public static String[] splitOnUnicodeWhitespace(CharSequence input) {
return terms.toArray(new String[0]);
}

/**
* Splits {@code input} on runs of ASCII whitespace with the result of
* {@code String.split("\\s+")}: a leading run yields one empty first element, trailing
* empty elements are dropped, whitespace-only input yields an empty array, and empty
* input yields a single empty element. This is a character scan, not a regular
* expression.
*
* @param input The text to split. Must not be {@code null}.
* @return The elements in order.
* @throws IllegalArgumentException If {@code input} is {@code null}.
*/
public static String[] splitOnAsciiWhitespace(String input) {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
if (input.isEmpty()) {
return new String[] {""};
}
final List<String> elements = new ArrayList<>();
if (isAsciiWhitespace(input.charAt(0))) {
elements.add("");
}
int start = 0;
for (int i = 0; i < input.length(); i++) {
if (isAsciiWhitespace(input.charAt(i))) {
if (i > start) {
elements.add(input.substring(start, i));
}
while (i + 1 < input.length() && isAsciiWhitespace(input.charAt(i + 1))) {
i++;
}
start = i + 1;
}
}
if (input.length() > start) {
elements.add(input.substring(start));
}
while (!elements.isEmpty() && elements.get(elements.size() - 1).isEmpty()) {
elements.remove(elements.size() - 1);
}
return elements.toArray(new String[0]);
}

/**
* Tests whether {@code input} contains an ASCII capital letter, {@code A} to {@code Z}.
* Capitals outside ASCII do not count. This is a character scan, not a regular expression.
*
* @param input The text to check. Must not be {@code null}.
* @return {@code true} if at least one character is an ASCII capital letter.
* @throws IllegalArgumentException If {@code input} is {@code null}.
*/
public static boolean containsAsciiUpperCase(CharSequence input) {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
for (int i = 0; i < input.length(); i++) {
final char c = input.charAt(i);
if (c >= 'A' && c <= 'Z') {
return true;
}
}
return false;
}

/**
* Tests whether {@code input} contains an ASCII digit, {@code 0} to {@code 9}. Digits outside
* ASCII do not count. This is a character scan, not a regular expression.
*
* @param input The text to check. Must not be {@code null}.
* @return {@code true} if at least one character is an ASCII digit.
* @throws IllegalArgumentException If {@code input} is {@code null}.
*/
public static boolean containsAsciiDigit(CharSequence input) {
if (input == null) {
throw new IllegalArgumentException("input must not be null");
}
for (int i = 0; i < input.length(); i++) {
final char c = input.charAt(i);
if (c >= '0' && c <= '9') {
return true;
}
}
return false;
}

/**
* Trims leading and trailing runs of Unicode {@code White_Space}, the same set
* {@link #splitOnUnicodeWhitespace(CharSequence)} breaks terms on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -61,23 +60,71 @@ public String getHelp() {
+ "Defaults to a WhitespaceTokenizer.";
}

private static final Pattern UNTOKENIZED_PAREN_PATTERN_1 = Pattern.compile("([^ ])([({)}])");
private static final Pattern UNTOKENIZED_PAREN_PATTERN_2 = Pattern.compile("([({)}])([^ ])");

public static Parse[] parseLine(String line, Parser parser, int numParses) {
return parseLine( line, parser, WhitespaceTokenizer.INSTANCE, numParses );
}

public static Parse[] parseLine(String line, Parser parser, Tokenizer tokenizer, int numParses) {
// fix some parens patterns
line = UNTOKENIZED_PAREN_PATTERN_1.matcher(line).replaceAll("$1 $2");
line = UNTOKENIZED_PAREN_PATTERN_2.matcher(line).replaceAll("$1 $2");
line = spaceUntokenizedParens(line);

// tokenize
String[] tokens = tokenizer.tokenize(line);
return parseLine(tokens, parser, numParses);
}

/**
* Separates round and curly brackets from adjacent text in two left-to-right passes: the
* first puts a space between a non-space character and a following bracket, the second
* between a bracket and a following non-space character. Each pass resumes after the pair
* it just spaced, so a pair overlapping that match is only seen by the second pass.
*
* @param line The untokenized line.
* @return The spaced line.
*/
static String spaceUntokenizedParens(String line) {
return insertParenSpaces(insertParenSpaces(line, false), true);
}

/**
* Inserts a space between a bracket and an adjacent non-space character, left to right.
*
* @param line The untokenized line.
* @param parenFirst {@code true} to space a bracket before a character, {@code false} after one.
* @return The spaced line.
*/
private static String insertParenSpaces(String line, boolean parenFirst) {
StringBuilder spaced = new StringBuilder(line.length() + 8);
int i = 0;
while (i < line.length()) {
char c = line.charAt(i);
if (i + 1 < line.length()) {
char next = line.charAt(i + 1);
boolean match = parenFirst
? isParen(c) && next != ' '
: c != ' ' && isParen(next);
if (match) {
spaced.append(c).append(' ').append(next);
i += 2;
continue;
}
}
spaced.append(c);
i++;
}
return spaced.toString();
}

/**
* Tests for a round or curly bracket.
*
* @param c The character.
* @return {@code true} for {@code (}, {@code )}, <code>{</code>, or <code>}</code>.
*/
private static boolean isParen(char c) {
return c == '(' || c == ')' || c == '{' || c == '}';
}

/**
* Parses the specified pre-tokenized sentence and returns the requested number of parses
* or fewer.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package opennlp.tools.cmdline.parser;

import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import opennlp.tools.parser.Parse;
import opennlp.tools.parser.Parser;
import opennlp.tools.tokenize.WhitespaceTokenizer;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class ParserToolTest {

/*
* The first pass puts a space before a bracket that follows a non-space character, the
* second after a bracket that precedes one. Each pass resumes after the pair it spaced, so
* "((" gets its space from the second pass only, and only a space, not a tab, separates.
*/
private static Stream<Arguments> parenLines() {
return Stream.of(
Arguments.of("a(b)c", "a ( b ) c"),
Arguments.of("a (b) c", "a ( b ) c"),
Arguments.of("foo(bar){baz}", "foo ( bar ) {baz }"),
Arguments.of("((a)(b))", "( ( a ) (b ) )"),
Arguments.of("a(b(c)d)e", "a ( b ( c ) d ) e"),
Arguments.of("x((", "x ( ("),
Arguments.of("((x", "( ( x"),
Arguments.of("()", "( )"),
Arguments.of("(", "("),
Arguments.of("", ""),
Arguments.of("no parens here", "no parens here"),
Arguments.of("«quoted»", "«quoted»"),
Arguments.of("tab\there(", "tab\there ("),
Arguments.of("a (b", "a ( b"));
}

@ParameterizedTest
@MethodSource("parenLines")
void testSpaceUntokenizedParens(String line, String expected) {
assertEquals(expected, ParserTool.spaceUntokenizedParens(line));
}

@Test
void testParseLineSeparatesParensBeforeTokenizing() {
Parser echo = new Parser() {
@Override
public Parse[] parse(Parse tokens, int numParses) {
return new Parse[] {tokens};
}

@Override
public Parse parse(Parse tokens) {
return tokens;
}
};
Parse[] parses = ParserTool.parseLine("f(x)+g({y})", echo, WhitespaceTokenizer.INSTANCE, 1);
assertEquals(1, parses.length);
String[] tokens = Stream.of(parses[0].getChildren()).map(Parse::getCoveredText)
.toArray(String[]::new);
// "{y" stays joined: the second pass consumed "{" while spacing "( {"
assertArrayEquals(new String[] {"f", "(", "x", ")", "+g", "(", "{y", "}", ")"}, tokens);
}
}
Loading
Loading