[ 
https://issues.apache.org/jira/browse/OPENNLP-1989?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18120854#comment-18120854
 ] 

ASF GitHub Bot commented on OPENNLP-1989:
-----------------------------------------

rzo1 commented on code in PR #1321:
URL: https://github.com/apache/opennlp/pull/1321#discussion_r4136825586


##########
opennlp-core/opennlp-ml/opennlp-ml-commons/src/main/java/opennlp/tools/ml/model/RealValueFileEventStream.java:
##########
@@ -125,21 +131,41 @@ public static float[] parseContexts(String[] contexts) {
     return values;
   }
 
+  /**
+   * Parses one event line. Fields are separated by runs of space, tab, 
carriage return,
+   * line feed and form feed, as in {@link FileEventStream}; the first field 
is the outcome,
+   * the rest are contexts parsed by {@link #parseContexts(String[])}.
+   *
+   * @param line The event line. Must not be {@code null}.
+   * @return The event; a line with only an outcome gives an event without 
contexts.
+   * @throws IllegalArgumentException Thrown if {@code line} is {@code null} 
or a value is negative,
+   *         NaN or infinite.
+   * @throws InvalidFormatException Thrown if {@code line} is blank.
+   */
+  public static Event parseEvent(String line) throws InvalidFormatException {

Review Comment:
   The vertical tab was a separator here and in `RealBasicEventStream` 
(`split("\\s+")`) and isn't one anymore. That's only mentioned in a test 
comment. Please add it to the description next to the outcome-only change.



##########
opennlp-core/opennlp-ml/opennlp-ml-commons/src/test/java/opennlp/tools/ml/AbstractEventStreamTest.java:
##########
@@ -60,6 +60,14 @@ public abstract class AbstractEventStreamTest {
   protected static final String EVENTS_INVALID_NEGATIVE =
           "other wc=ic=-1.0 w&c=he,ic=-2.0 n1wc=lc=-3.0 n1w&c=belongs,lc=-4.0 
n2wc=lc=-5.0\n";
 
+  /**
+   * Characters that stay inside an event field: Unicode whitespace outside 
the delimiter set,
+   * the vertical tab, which {@code split("\\s+")} used to treat as a 
separator, a file separator,

Review Comment:
   "which `split("\\s+")` used to treat as a separator" is history. Say what 
the test checks.



##########
opennlp-docs/src/docbkx/machine-learning.xml:
##########
@@ -86,6 +86,22 @@ under the License.
                component which uses this functionality.
                </para>
                <para>
+               <code>FileEventStream</code> reads events from a text file, one 
event per line, and
+               <code>RealValueFileEventStream</code> and 
<code>RealBasicEventStream</code> read events
+               with real-valued features. The first field of a line is the 
outcome and each further field
+               is a context, optionally followed by <code>=</code> and its 
value after the last
+               <code>=</code>. Space, tab, carriage return, line feed and form 
feed separate fields; other
+               characters, including no-break spaces, stay inside feature 
names. A line with only an
+               outcome is an event without contexts, and a blank line is an 
error. Since 3.0, an outcome-only line

Review Comment:
   Much longer than its neighbours, please rewrap.



##########
opennlp-core/opennlp-ml/opennlp-ml-commons/src/test/java/opennlp/tools/ml/model/RealValueFileEventStreamTest.java:
##########
@@ -92,4 +81,193 @@ void testReset() throws IOException {
     } catch (UnsupportedOperationException expected) {
     }
   }
+
+  @Test
+  void testReadSplitsContextsOnWhitespaceRuns() throws IOException {
+    String input = "other wc=ic=1.0\t\tw&c=he,ic=2.0   n1wc=lc=3.0 \t \n"
+        + "other wc=lc=1.0 w&c=belongs,lc=2.0\u00A0p1wc=ic=3.0\n"
+        + "other   wc=lc=1.0  w&c=to,lc=2.0  \n";
+    try (ObjectStream<Event> eventStream = createEventStream(input)) {
+      Event e = eventStream.read();
+      Assertions.assertArrayEquals(
+          new String[] {"wc=ic", "w&c=he,ic", "n1wc=lc"}, e.getContext());
+      Assertions.assertArrayEquals(new float[] {1.0f, 2.0f, 3.0f}, 
e.getValues());
+      e = eventStream.read();
+      Assertions.assertArrayEquals(
+          new String[] {"wc=lc", "w&c=belongs,lc=2.0\u00A0p1wc=ic"}, 
e.getContext());
+      Assertions.assertArrayEquals(new float[] {1.0f, 3.0f}, e.getValues());
+      // repeated runs produce no empty predicate
+      e = eventStream.read();
+      Assertions.assertArrayEquals(
+          new String[] {"wc=lc", "w&c=to,lc"}, e.getContext());
+      Assertions.assertArrayEquals(new float[] {1.0f, 2.0f}, e.getValues());
+      Assertions.assertNull(eventStream.read());
+    }
+  }
+
+  private static Stream<Arguments> fieldSeparators() {
+    return Stream.of(
+        // the fixed event separators, including runs and leading separators
+        Arguments.of("other\twc=ic=1.0", "other", new String[] {"wc=ic"}),
+        Arguments.of("other  \t wc=ic=1.0", "other", new String[] {"wc=ic"}),
+        Arguments.of("other\fwc=ic=1.0", "other", new String[] {"wc=ic"}),
+        Arguments.of("  other wc=ic=1.0", "other", new String[] {"wc=ic"}),
+        Arguments.of("\tother wc=ic=1.0", "other", new String[] {"wc=ic"}),
+        Arguments.of("other wc=ic=1.0\r", "other", new String[] {"wc=ic"}),
+        // non-delimiter characters remain part of the outcome
+        Arguments.of("other\u00A0wc=ic=1.0", "other\u00A0wc=ic=1.0", new 
String[0]),
+        Arguments.of("other\u0085wc=ic=1.0", "other\u0085wc=ic=1.0", new 
String[0]),
+        Arguments.of("other\u2028wc=ic=1.0", "other\u2028wc=ic=1.0", new 
String[0]),
+        Arguments.of("other\u3000wc=ic=1.0", "other\u3000wc=ic=1.0", new 
String[0]),
+        // file separator and zero width space are not delimiters either
+        Arguments.of("other\u001Cwc=ic=1.0", "other\u001Cwc=ic=1.0", new 
String[0]),
+        Arguments.of("other\u200Bwc=ic=1.0", "other\u200Bwc=ic=1.0", new 
String[0]),
+        Arguments.of("other wc\u001C=ic=1.0", "other", new String[] 
{"wc\u001C=ic"}),
+        // a supplementary character is one character of a field
+        Arguments.of("\uD83D\uDE00 w=\uD83D\uDE00=1.0", "\uD83D\uDE00", new 
String[] {"w=\uD83D\uDE00"}));
+  }
+
+  @ParameterizedTest
+  @MethodSource("fieldSeparators")
+  void testFieldsUseEventDelimiters(String line, String outcome, String[] 
contexts)
+      throws IOException {
+    Event e = RealValueFileEventStream.parseEvent(line);
+    Assertions.assertEquals(outcome, e.getOutcome());
+    Assertions.assertArrayEquals(contexts, e.getContext());
+  }
+
+  @Test
+  void testParseEventRejectsNull() {
+    IllegalArgumentException e = 
Assertions.assertThrows(IllegalArgumentException.class,
+        () -> RealValueFileEventStream.parseEvent(null));
+    Assertions.assertEquals("line must not be null", e.getMessage());
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"other", "other ", " other\t"})
+  void testOutcomeOnlyLineHasNoContexts(String line) throws IOException {
+    Event e = RealValueFileEventStream.parseEvent(line);
+    Assertions.assertEquals("other", e.getOutcome());
+    Assertions.assertEquals(0, e.getContext().length);
+    Assertions.assertNull(e.getValues());
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"", " ", "\t", "\f", " \t\r\n\f", "\r"})
+  void testLineWithoutOutcomeIsRejected(String line) {
+    InvalidFormatException e = 
Assertions.assertThrows(InvalidFormatException.class,
+        () -> RealValueFileEventStream.parseEvent(line));
+    Assertions.assertEquals("An event line must start with an outcome: \"" + 
line + "\"", e.getMessage());
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"org/start", "a=b", "x;y", "B-ORG", "\u00E9tat", 
"\uD83D\uDE00"})
+  void testOutcomeMayContainAnyNonWhitespace(String outcome) throws 
IOException {
+    Event e = RealValueFileEventStream.parseEvent(outcome + " wc=ic=1.0");
+    Assertions.assertEquals(outcome, e.getOutcome());
+    Assertions.assertArrayEquals(new String[] {"wc=ic"}, e.getContext());
+  }
+
+  private static Stream<Arguments> contextValues() {
+    return Stream.of(
+        Arguments.of("w&c=he,ic=2.0", "w&c=he,ic", 2.0f),
+        Arguments.of("a=b=c=3.5", "a=b=c", 3.5f),
+        Arguments.of("wc=ic=1e2", "wc=ic", 100f),
+        Arguments.of("wc=ic=1E-2", "wc=ic", 0.01f),
+        Arguments.of("wc=ic=.5", "wc=ic", 0.5f),
+        Arguments.of("wc=ic=+2", "wc=ic", 2f),
+        Arguments.of("wc=ic=0", "wc=ic", 0f));
+  }
+
+  /** The value follows the last {@code =}, so a context name may contain the 
character. */
+  @ParameterizedTest
+  @MethodSource("contextValues")
+  void testContextValueFollowsTheLastEqualsSign(String context, String name, 
float value)
+      throws IOException {
+    Event e = RealValueFileEventStream.parseEvent("other " + context);
+    Assertions.assertArrayEquals(new String[] {name}, e.getContext());
+    Assertions.assertArrayEquals(new float[] {value}, e.getValues());
+  }
+
+  @ParameterizedTest
+  // no equals sign, an empty value after it, an equals sign in first 
position, text or a comma after it
+  @ValueSource(strings = {"wc", "wc=", "=5", "wc=abc", "wc=1,5", 
"wc=ic=1.0.0"})
+  void testContextWithoutANumberIsKeptWholeAndCountsOnce(String context) 
throws IOException {
+    Event e = RealValueFileEventStream.parseEvent("other " + context);
+    Assertions.assertArrayEquals(new String[] {context}, e.getContext());
+    Assertions.assertNull(e.getValues());
+  }
+
+  @Test
+  void testContextsWithAndWithoutValuesMix() throws IOException {
+    Event e = RealValueFileEventStream.parseEvent("other wc=ic=2.0 wc 
n1wc=lc=0.5");
+    Assertions.assertArrayEquals(new String[] {"wc=ic", "wc", "n1wc=lc"}, 
e.getContext());
+    Assertions.assertArrayEquals(new float[] {2.0f, 1.0f, 0.5f}, 
e.getValues());
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"wc=ic=-1", "wc=ic=-0.5", "wc=ic=-1e2", 
"wc=ic=-1E-2"})
+  void testNegativeValueIsRejectedWithTheContextNamed(String context) {
+    IllegalArgumentException e = 
Assertions.assertThrows(IllegalArgumentException.class,
+        () -> RealValueFileEventStream.parseEvent("other " + context));
+    Assertions.assertEquals("Negative values are not allowed: " + context, 
e.getMessage());
+  }
+
+  /** {@code Float.parseFloat} accepts these, but they are no usable feature 
values. */
+  @ParameterizedTest
+  @ValueSource(strings = {"wc=ic=NaN", "wc=ic=Infinity", "wc=ic=-Infinity"})
+  void testNonFiniteValueIsRejectedWithTheContextNamed(String context) {
+    IllegalArgumentException e = 
Assertions.assertThrows(IllegalArgumentException.class,
+        () -> RealValueFileEventStream.parseEvent("other " + context));
+    Assertions.assertEquals("Values must be finite: " + context, 
e.getMessage());
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"\n", "\r\n", "\r"})
+  void testReadAcceptsEveryLineTerminator(String terminator) throws 
IOException {
+    String input = "other wc=ic=1.0" + terminator + "other wc=lc=2.0" + 
terminator;
+    try (ObjectStream<Event> eventStream = createEventStream(input)) {
+      Assertions.assertArrayEquals(new String[] {"wc=ic"}, 
eventStream.read().getContext());
+      Assertions.assertArrayEquals(new String[] {"wc=lc"}, 
eventStream.read().getContext());
+      Assertions.assertNull(eventStream.read());
+    }
+  }
+
+  @Test
+  void testReadRejectsBlankLine() throws IOException {
+    try (ObjectStream<Event> eventStream = createEventStream("other 
wc=ic=1.0\n\nother wc=lc=1.0\n")) {
+      Assertions.assertEquals("other", eventStream.read().getOutcome());
+      Assertions.assertThrows(InvalidFormatException.class, eventStream::read);
+    }
+  }
+
+  /** See {@link #NON_DELIMITER_CHARS}: only the vertical tab row pins a 
change from 2.x. */

Review Comment:
   "pins a change from 2.x" is history. Say what the test checks.





> Regex removal (5b/10): Make event parsing deterministic and preserve feature 
> identities
> ---------------------------------------------------------------------------------------
>
>                 Key: OPENNLP-1989
>                 URL: https://issues.apache.org/jira/browse/OPENNLP-1989
>             Project: OpenNLP
>          Issue Type: Improvement
>            Reporter: Kristian Rickert
>            Priority: Major
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> Correct parsing in {{RealValueFileEventStream}}, {{RealBasicEventStream}}, 
> and {{SimpleEventStreamBuilder}} using explicit, documented delimiters 
> independent of global tokenizer settings. Preserve Unicode characters within 
> feature names, eliminate accidental empty features, and prevent outcome-only 
> events from prematurely ending a stream. Report malformed records clearly and 
> document the shared parsing API. Test exact outcomes, feature arrays, values, 
> and continued reading after outcome-only events.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to