Skip to content

OPENNLP-1875: Align whitespace handling with the Unicode White_Space property#1150

Open
krickert wants to merge 8 commits into
apache:mainfrom
ai-pipestream:whitespace-ucd
Open

OPENNLP-1875: Align whitespace handling with the Unicode White_Space property#1150
krickert wants to merge 8 commits into
apache:mainfrom
ai-pipestream:whitespace-ucd

Conversation

@krickert

@krickert krickert commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

UCD-backs the whitespace predicates outside the OPENNLP-1850 engine (OPENNLP-1852 consistency item; generalizes the OPENNLP-205 correction). Every StringUtil.isWhitespace, Character.isWhitespace, Character.isSpaceChar, and \s-split site outside the 1850 engine was audited and classified:

  • Migrated to the UCD White_Space set: both rule-based tokenizers, Span.trim, the sentence detector's abbreviation guard, the stopword and spellcheck CLIs, SymSpell.lookupCompound, the spellcheck PER_TOKEN normalizer, and the UIMA NumberUtil.
  • Frozen with documented rationale and permanent pins: trained-model feature generation and the BERT reference definition (model stability).
  • Kept JVM/ASCII semantics with per-site justification comments: format parsers, where the format fixes the definition.
  • StringUtil.isWhitespace itself stays frozen as the documented legacy predicate, since its remaining callers are exactly the frozen sites; user-text code is directed to UnicodeWhitespace.

Method is characterize-then-refactor: the first commit pins pre-change behavior at NBSP, figure and narrow space, U+2028/U+2029, NEL, and U+001C..U+001F for every touched site, so the migration commits flip only the deliberate deltas.

This is a behavior change; the release-note text is in the JIRA issue.

Verification: opennlp-runtime 1313/0, plus green suites in api, formats, cli, tools, uima, and spellcheck.

https://issues.apache.org/jira/browse/OPENNLP-1875

@mawiesne mawiesne marked this pull request as draft July 8, 2026 04:32
@krickert krickert force-pushed the whitespace-ucd branch 2 times, most recently from c680c40 to 6fa614e Compare July 8, 2026 14:44
@krickert krickert marked this pull request as ready for review July 8, 2026 15:04
@krickert krickert requested review from atarora, jzonthemtn, mawiesne and rzo1 and removed request for mawiesne July 8, 2026 15:04
krickert added 5 commits July 8, 2026 15:05
Characterization tests ahead of migrating the user-text whitespace
predicates to the Unicode White_Space set. Every expectation in this
commit captures the behavior of the current implementation; the sites
classified for migration flip together with the code change in the
follow-up commits, so the review diff shows the exact behavior delta.

Audit of StringUtil.isWhitespace, Character.isWhitespace,
Character.isSpaceChar and \s regex splits outside the OPENNLP-1850
engine (opennlp/tools/util/normalizer, opennlp/tools/tokenize/uax29),
the four legacy CharSequenceNormalizers and the opennlp-dl module:

Migrate to Unicode White_Space (user-text paths):
* WhitespaceTokenizer.tokenizePos (opennlp-api)
* SimpleTokenizer.tokenizePos (opennlp-runtime)
* Span.trim (opennlp-api)
* SentenceDetectorME.isAcceptableBreak whitespace guard before an
  abbreviation (opennlp-runtime)
* StopwordFilterTool stdin line split (opennlp-cli)
* SymSpell.lookupCompound term split (opennlp-spellcheck)
* SpellCheckingCharSequenceNormalizer PER_TOKEN boundaries
  (opennlp-spellcheck)
* CorrectTextTool -suggest token split (opennlp-spellcheck)
* NumberUtil.parse whitespace strip (opennlp-uima)

Keep frozen, feature generation for existing trained models or a
reference-fixed definition (pinned permanently in this commit):
* DefaultSDContextGenerator sp/sn features (opennlp-runtime)
* DefaultTokenContextGenerator _ws feature (opennlp-runtime)
* BertNormalization.isWhitespace, fixed by the reference BERT
  implementation (opennlp-api)
* StringUtil.isWhitespace itself: after the migration its remaining
  in-tree callers are exactly the frozen feature generators and the
  format parsers below, so the predicate keeps its current semantics
  and the user-text call sites move to the Unicode set instead

Keep, config/format parsing where JVM or ASCII-regex semantics belong
to the format rather than to user text:
* ConlluTokenSampleStream text comment alignment (CoNLL-U)
* SgmlParser tag and attribute scanning (MUC SGML)
* TokenSampleStream lang/en (PTB-style training format)
* DictionaryStopwordFilter stopword list files
* DownloadUtil checksum file split
* ADNameSampleStream, ADPOSSampleStream, ADSentenceStream (AD corpus)
* NameSample START_TAG_PATTERN and Parse tokenPattern (annotation
  formats)
* RegexNameFinderFactory \s inside structured entity patterns
  (regex-by-design component)
* StringTokenizer sites: Dictionary, HeadRules,
  AncoraSpanishHeadRules, AbstractModelReader, FileEventStream,
  GenerateManualTool (model and resource file formats)
* Test-only \s+ splits of ASCII fixtures
…sers

StringUtil.isWhitespace keeps its exact current semantics (the union of
Character.isWhitespace and the Zs category). The audit showed that after
the user-text migration its remaining in-tree callers are exactly the
sites that must not change:

* DefaultSDContextGenerator and DefaultTokenContextGenerator generate
  feature strings that are baked into every trained sentence-detector
  and tokenizer model; moving them to the Unicode White_Space set would
  silently change features for existing models. Both now carry an
  intentional-exception note and are pinned by tests.
* The format parsers (CoNLL-U alignment, MUC SGML markup, stopword list
  files, PTB-style training data, AD corpus fields, checksum files, the
  <START:type> annotation markup, the PTB bracket grammar) parse fixed
  formats where ASCII or JVM whitespace is the format's own definition.
  Each retained site now carries a one-line justification.
* BertNormalization.isWhitespace is fixed by the reference BERT
  implementation; pre-trained vocabularies depend on it.
* RegexNameFinderFactory applies regex to text by design; the embedded
  \s classes are part of the entity grammar.

The StringUtil javadoc now states the frozen definition, its exact
deltas against the Unicode White_Space property (includes U+001C..
U+001F, excludes U+0085), the model-stability rationale, and points
user-text code at UnicodeWhitespace.isWhitespace / CharClass.whitespace
as the standards-sourced alternative delivered by OPENNLP-1850. No
behavior change in this commit.
Move the user-text whitespace predicates of the rule-based components
onto the standards-sourced Unicode White_Space set delivered by
OPENNLP-1850 (UnicodeWhitespace/CharClass), generalizing the
OPENNLP-205 sentence-detector correction:

* WhitespaceTokenizer.tokenizePos (opennlp-api)
* SimpleTokenizer.tokenizePos (opennlp-runtime)
* Span.trim (opennlp-api)
* SentenceDetectorME.isAcceptableBreak: the whitespace guard before an
  abbreviation now uses the detector-wide WHITESPACE CharClass instead
  of Character.isWhitespace, so all detector stages share one
  whitespace definition

Behavior delta, pinned by the flipped characterization tests: the next
line control U+0085 now counts as whitespace (separates tokens, is
trimmed from spans, protects an abbreviation), and the U+001C..U+001F
information separators no longer do. For the abbreviation guard the
no-break spaces (U+00A0, U+2007, U+202F) also start counting as
whitespace, because that site previously used the narrower
Character.isWhitespace. NBSP handling in the tokenizers and Span.trim
is unchanged (the legacy predicate already included the Zs category).

Note for existing models: WhitespaceTokenizer output feeds TokenizerME
candidate spans and BPETokenizer pre-tokenization, so tokens produced
for text containing the affected code points can differ from earlier
releases. Feature generation itself stays on the frozen legacy
predicate (see the previous commit).
…pace

Replace the ASCII \s regex whitespace handling on the remaining
user-text paths with the standards-sourced Unicode White_Space set from
OPENNLP-1850:

* StopwordFilterTool and CorrectTextTool -suggest tokenize input lines
  with WhitespaceTokenizer (now White_Space-backed) instead of
  split("\\s+"); this also drops the empty leading token the regex
  produced for lines starting with whitespace
* SymSpell.lookupCompound splits terms with a cursor scan on
  UnicodeWhitespace instead of trim().split("\\s+")
* SpellCheckingCharSequenceNormalizer PER_TOKEN boundaries move from
  Character.isWhitespace to UnicodeWhitespace (whitespace runs are
  still copied verbatim, so offsets are preserved)
* NumberUtil.parse (opennlp-uima) strips whitespace with a cursor scan
  on UnicodeWhitespace instead of a \s regex pass, so the no-break
  spaces several locales use as digit grouping separators no longer
  stop the number parse

Behavior delta, pinned by the flipped characterization tests: for the
regex sites the change is purely additive (NBSP, figure/narrow space,
NEL, the Unicode line and paragraph separators and the other Zs
separators now separate or strip; U+001C..U+001F never matched \s and
still do not split). For the PER_TOKEN normalizer, which used
Character.isWhitespace, U+001C..U+001F additionally stop being token
boundaries. None of these components feeds trained-model features.
@krickert krickert force-pushed the whitespace-ucd branch 3 times, most recently from 6fa614e to 676e5de Compare July 9, 2026 20:03
@rzo1

rzo1 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Hi @krickert - as mentioned on Slack I currently dont have the time for a manual review but I just let Fable do a comprehensive review on this PR. Here is the result:

  • opennlp-api/src/main/java/opennlp/tools/tokenize/WhitespaceTokenizer.java: Sample parsers that split training/annotation lines via WhitespaceTokenizer.INSTANCE (NameSample.parse, POSSample, TokenSample, brat/ontonotes/doccat streams) silently inherit the Unicode White_Space migration. Existing corpora containing U+001C..U+001F or U+0085 now produce different samples (and shifted NameSample token indices). These indirect format-parsing sites fall outside the PR's audit of direct call sites and received neither a freeze, a justification comment, nor a characterization pin, unlike the analogous direct sites (ConlluTokenSampleStream, SgmlParser, AD streams) that were frozen for exactly this reason.
  • opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/tokenize/WhitespaceTokenizerTest.java: The interaction of keepNewLines=true with U+0085 (newly whitespace in this PR) is untested and undocumented in both WhitespaceTokenizer and SimpleTokenizer. The private isLineSeparator guard matches only \n and \r, so U+0085/U+2028/U+2029 now split tokens but are silently dropped instead of being emitted as newline tokens; every new test sets setKeepNewLines(false), so this asymmetry is neither pinned by a test nor mentioned in the updated javadoc. Users relying on keepNewLines to reconstruct line structure get silent data loss for NEL/LS/PS input.
  • opennlp-extensions/opennlp-spellcheck/src/main/java/opennlp/spellcheck/symspell/SymSpell.java: lookupCompound now splits terms on Unicode White_Space but computes the final reported edit distance against input.trim(), which strips only chars <= U+0020. Leading/trailing NBSP/NEL runs inflate the reported distance (an NBSP-only input yields distance 1 instead of 0, unlike an ASCII-space-only input). The trim on the distance path should use the same Unicode set as the split.
  • opennlp-extensions/opennlp-spellcheck/src/main/java/opennlp/spellcheck/cmdline/CorrectTextTool.java: In listSuggestions the blank-line guard line.isBlank() still uses JVM whitespace while tokenization uses the Unicode set, so an NBSP-only line skips the newline branch and then tokenizes to zero tokens: it disappears from the output entirely, whereas an ASCII-blank line emits an empty line. Guard with the same Unicode whitespace definition or emit a newline when tokenize() returns empty.
  • opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/sentdetect/SentenceDetectorME.java and opennlp-extensions/opennlp-uima/src/main/java/opennlp/uima/normalizer/NumberUtil.java: The whitespace-semantics change is not reflected in the published javadoc of the affected API surface. isAcceptableBreak is protected and designed for override but documents the change only in inline comments; NumberUtil.parse now strips all Unicode White_Space (including NBSP/NNBSP grouping separators) but only the private helper mentions it. Both should get the same "Since 3.0 ... Unicode White_Space" note the other migrated public sites received.
  • opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/tokenize/SimpleTokenizerTest.java: The private cp(int codePoint) helper is duplicated across eight test files touched by this PR (four within opennlp-runtime alone). Consider a shared test utility, or simply Character.toString(int), which makes the helper unnecessary.

Human review will follow.

…tors, spellcheck consistency

The sample parsers that split annotation lines through the shared
WhitespaceTokenizer (NameSample, POSSample, TokenSample, the brat,
OntoNotes and doccat streams) now carry the same per-site justification
comment as the frozen direct call sites, and characterization tests pin
that a U+0085 separated line parses identically to its ASCII-space
equivalent, spans included.

keepNewLines now emits the next line control U+0085 and the Unicode line
and paragraph separators as newline tokens instead of silently dropping
them, in both WhitespaceTokenizer and SimpleTokenizer, with the emitted
set documented in the javadoc; the SimpleTokenizer path also anchors the
emitted span at the separator's own position. SymSpell's compound
distance now trims with the same Unicode set it splits with, so a
leading or trailing NBSP no longer inflates the reported distance, and
the spellcheck command line emits an empty line for a whitespace-only
input line instead of swallowing it. SentenceDetectorME.isAcceptableBreak
and NumberUtil.parse carry the same since-3.0 Unicode White_Space note
as the other migrated public sites. Tests cover each changed behavior.
@krickert

Copy link
Copy Markdown
Contributor Author

Thanks. Addressed in 9fd57a8, one pushback:

  • Indirect sample-parser sites: each now carries the same per-site justification comment as the frozen direct call sites, and characterization tests pin the new behavior where it is deliberate: a NameSample, POSSample, and TokenSample line separated by U+0085 parses identically to its ASCII-space equivalent, token indices and spans included. The brat, OntoNotes, and doccat streams got the comment treatment; their separators are format-fixed ASCII in practice, and the comments say so.
  • keepNewLines and NEL/LS/PS: fixed, not just documented. The line separator set under keepNewLines is now the ASCII pair plus U+0085, U+2028, and U+2029 in both WhitespaceTokenizer and SimpleTokenizer, so those code points are emitted as newline tokens instead of silently dropped; the SimpleTokenizer path also anchors the emitted span at the separator's own position. Javadoc states the emitted set, tests pin it with keepNewLines enabled.
  • SymSpell compound distance: the distance path now trims with the same Unicode White_Space set the split uses; an NBSP-only input reports distance 0 like an ASCII-space-only input, tested.
  • CorrectTextTool blank guard: a whitespace-only line now emits an empty output line instead of disappearing; guarded with the same Unicode definition as tokenization, tested through the command line tool.
  • SentenceDetectorME.isAcceptableBreak and NumberUtil.parse: both carry the since-3.0 Unicode White_Space javadoc note the other migrated public sites received.
  • cp(int) duplication: kept as-is. The three-line helper names the intent at call sites full of hex literals, and consolidating across modules would need a shared test artifact for a helper that small; the duplication predates this PR across the suite and is better swept in one test-wide cleanup than piecemeal here.

@krickert krickert self-assigned this Jul 10, 2026

@rzo1 rzo1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A) Please reduce the inline commentary.

Nearly every touched call site carries a multi-line narrative comment ("Sample parsing deliberately shares…", "Resource-format parsing: …", "Format parsing: the training data is…", the restated whitespace-delta notes in SentenceDetectorME, SymSpell, NumberUtil, etc.). These explain the migration decision, not the code.

IMHO that rationale belongs in the JIRA issue / this PR description / the release notes, where it's already captured. In the code it's noise that will go stale and doesn't help the next reader. (This can be taken verbatim to all other open PRs as well)

Concretely:

  • Drop the per-call-site justification comments entirely (format parsers, sample parsers, CLI tools, isLineSeparator, the repeated "since 3.0…" delta enumerations at call sites).
  • Keep, but condense to 1–3 lines, the notes that document a real invariant or user-visible behavior change: the frozen-predicate rationale on StringUtil.isWhitespace and the two context generators, the BERT-reference pin in BertNormalization, and a short "whitespace is the Unicode White_Space set since 3.0" note in the javadoc of WhitespaceTokenizer, SimpleTokenizer and Span#trim. One sentence plus a pointer to OPENNLP-1875 is enough; the full U+xxxx enumerations don't need to live in javadoc.
  • The comment explaining the SimpleTokenizer span-position fix in keepNewLines mode can stay (it explains a non-obvious bug fix), but one line suffices.

B) Please expose the new predicate via StringUtil and use that at the migrated call sites.

I'd like to avoid splitting the whitespace predicates across two entry points: people reach for StringUtil by habit, and having the legacy and the standard predicate side by side (with distinct names) makes the difference discoverable. UnicodeWhitespace should stay as the implementation/reference-data home; just add a delegating facade:

/**
 * Unicode {@code White_Space} membership; delegates to
 * {@link opennlp.tools.util.normalizer.UnicodeWhitespace#isWhitespace(int)}.
 */
public static boolean isUnicodeWhitespace(int codePoint) {
  return UnicodeWhitespace.isWhitespace(code
}          

(plus a char overload to mirror the existing pair), and switch the migrated call sites (WhitespaceTokenizer, SimpleTokenizer, Span#trim, spellcheck, UIMAdeWhitespace.isWhitespace(…) toStringUtil.isUnicodeWhitespace(…). The CharClass.whitespace() usage in SentenceDetectorME is fine as is.

C) I think the issue and PR would benefit from a clearer title / heading

The PR title (and the issue title) is hard to understand; something like

  • Align whitespace handling with the Unicode White_Space property
  • Migrate whitespace checks on user text to Unicode White_Space

would be better understandable and cleaner in release notes.

@krickert krickert changed the title OPENNLP-1875: UCD-back the whitespace predicates: migrate user-text call sites, freeze the documented exceptions OPENNLP-1875: Align whitespace handling with the Unicode White_Space property Jul 12, 2026
…ences

Applies the review conventions from the OPENNLP-1869 review: frozen-predicate notes shrink to one contract sentence, JIRA keys and design narrative leave the sources, and the private helpers gain javadoc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants