This is an automated email from the ASF dual-hosted git repository.

krickert pushed a commit to branch light-stemmers
in repository https://gitbox.apache.org/repos/asf/opennlp.git

commit 2d00a837d54b2a26322605ff4ee0964a53fb68df
Author: Kristian Rickert <[email protected]>
AuthorDate: Thu Jul 16 06:21:14 2026 -0400

    OPENNLP-1886: Share the char-array stemming shell, cite the algorithms with 
verified links, drop the dangling deprecation
---
 .../stemmer/light/AbstractCharArrayStemmer.java    | 52 ++++++++++++++++
 .../tools/stemmer/light/EnglishMinimalStemmer.java | 27 +++------
 .../tools/stemmer/light/FinnishLightStemmer.java   | 45 ++++----------
 .../tools/stemmer/light/FrenchLightStemmer.java    | 32 ++++------
 .../tools/stemmer/light/FrenchMinimalStemmer.java  | 30 ++++------
 .../tools/stemmer/light/GermanLightStemmer.java    | 29 +++------
 .../tools/stemmer/light/GermanMinimalStemmer.java  | 29 +++------
 .../tools/stemmer/light/HungarianLightStemmer.java | 45 ++++----------
 .../tools/stemmer/light/ItalianLightStemmer.java   | 29 +++------
 .../tools/stemmer/light/NorwegianLightStemmer.java | 42 +++++--------
 .../stemmer/light/NorwegianMinimalStemmer.java     | 41 +++++--------
 .../stemmer/light/PortugueseLightStemmer.java      | 30 ++++------
 .../tools/stemmer/light/RussianLightStemmer.java   | 30 ++++------
 .../tools/stemmer/light/SpanishLightStemmer.java   | 29 +++------
 .../tools/stemmer/light/SpanishMinimalStemmer.java | 32 ++++------
 .../opennlp/tools/stemmer/light/StemmerUtil.java   | 69 +++++-----------------
 .../tools/stemmer/light/SwedishLightStemmer.java   | 30 ++++------
 .../tools/stemmer/light/SwedishMinimalStemmer.java | 33 ++++-------
 .../stemmer/light/LightStemmerParityTest.java      |  6 +-
 19 files changed, 249 insertions(+), 411 deletions(-)

diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/AbstractCharArrayStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/AbstractCharArrayStemmer.java
new file mode 100644
index 000000000..16f994c19
--- /dev/null
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/AbstractCharArrayStemmer.java
@@ -0,0 +1,52 @@
+/*
+ * 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.stemmer.light;
+
+import opennlp.tools.stemmer.Stemmer;
+
+/**
+ * Base class of the light and minimal stemmers. It implements the public
+ * {@link Stemmer#stem(CharSequence)} contract once: the word is validated, 
copied into a
+ * {@code char[]} buffer, stemmed in place by the concrete algorithm via 
{@link #stem(char[], int)},
+ * and returned as a {@link String} of the resulting length.
+ */
+abstract class AbstractCharArrayStemmer implements Stemmer {
+
+  /** {@inheritDoc} */
+  @Override
+  public CharSequence stem(CharSequence word) {
+    if (word == null) {
+      throw new IllegalArgumentException("word must not be null");
+    }
+    final int length = word.length();
+    final char[] buffer = new char[length];
+    for (int i = 0; i < length; i++) {
+      buffer[i] = word.charAt(i);
+    }
+    final int stemmedLength = stem(buffer, length);
+    return new String(buffer, 0, stemmedLength);
+  }
+
+  /**
+   * Stems the buffer prefix in place.
+   *
+   * @param s   The buffer holding the word; the algorithm may overwrite 
characters in place.
+   * @param len The filled length of the buffer.
+   * @return The length of the stem within {@code s}.
+   */
+  abstract int stem(char[] s, int len);
+}
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/EnglishMinimalStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/EnglishMinimalStemmer.java
index d1f9bb6f8..500599865 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/EnglishMinimalStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/EnglishMinimalStemmer.java
@@ -20,10 +20,13 @@ package opennlp.tools.stemmer.light;
 import opennlp.tools.commons.ThreadSafe;
 import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
+
 /**
  * Minimal plural stemmer for English.
  *
- * <p>This stemmer implements the "S-Stemmer" from <i>How Effective Is 
Suffixing?</i> Donna Harman.
+ * <p>This stemmer implements the "S-Stemmer" from
+ * <a 
href="https://doi.org/10.1002/(SICI)1097-4571(199101)42:1%3C7::AID-ASI2%3E3.0.CO;2-P">
+ * <i>How Effective Is Suffixing?</i></a> by Donna Harman (JASIS 42(1), 1991).
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -31,30 +34,18 @@ import opennlp.tools.stemmer.StemmerFactory;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class EnglishMinimalStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class EnglishMinimalStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
+  /** {@inheritDoc} */
+  @Override
   @SuppressWarnings("fallthrough")
-  private int stem(char[] s, int len) {
+  int stem(char[] s, int len) {
     if (len < 3 || s[len - 1] != 's') return len;
 
     switch (s[len - 2]) {
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FinnishLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FinnishLightStemmer.java
index b1035ace6..d06e6ff1f 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FinnishLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FinnishLightStemmer.java
@@ -57,11 +57,16 @@ import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.delete;
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+import static opennlp.tools.stemmer.light.StemmerUtil.isVowel;
+
 /**
  * Light Stemmer for Finnish.
  *
- * <p>This stemmer implements the algorithm described in: <i>Report on 
CLEF-2003 Monolingual
- * Tracks</i> Jacques Savoy
+ * <p>This stemmer implements the algorithm described in:
+ * <a 
href="https://ceur-ws.org/Vol-1169/CLEF2003wn-adhoc-Savoy2003b.pdf";><i>Report 
on CLEF-2003
+ * Monolingual Tracks</i></a> by Jacques Savoy (CLEF 2003 working notes). The 
inline suffix
+ * literals mirror the suffix tables of the cited paper and are kept inline 
for fidelity to the
+ * published algorithm.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -69,29 +74,17 @@ import static 
opennlp.tools.stemmer.light.StemmerUtil.endsWith;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class FinnishLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class FinnishLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len < 4) return len;
 
     for (int i = 0; i < len; i++)
@@ -247,18 +240,4 @@ public final class FinnishLightStemmer implements Stemmer, 
StemmerFactory {
 
     return len;
   }
-
-  private boolean isVowel(char ch) {
-    switch (ch) {
-      case 'a':
-      case 'e':
-      case 'i':
-      case 'o':
-      case 'u':
-      case 'y':
-        return true;
-      default:
-        return false;
-    }
-  }
 }
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchLightStemmer.java
index 38f11138a..2efea82e7 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchLightStemmer.java
@@ -57,11 +57,15 @@ import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.delete;
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+
 /**
  * Light Stemmer for French.
  *
- * <p>This stemmer implements the "UniNE" algorithm in: <i>Light Stemming 
Approaches for the French,
- * Portuguese, German and Hungarian Languages</i> Jacques Savoy
+ * <p>This stemmer implements the "UniNE" algorithm in:
+ * <a href="https://doi.org/10.1145/1141277.1141523";><i>Light Stemming 
Approaches for the French,
+ * Portuguese, German and Hungarian Languages</i></a> by Jacques Savoy (ACM 
SAC 2006). The inline
+ * suffix literals mirror the suffix tables of the cited paper and are kept 
inline for fidelity
+ * to the published algorithm.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -69,29 +73,17 @@ import static 
opennlp.tools.stemmer.light.StemmerUtil.endsWith;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class FrenchLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class FrenchLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len > 5 && s[len - 1] == 'x') {
       if (s[len - 3] == 'a' && s[len - 2] == 'u' && s[len - 4] != 'e') s[len - 
2] = 'l';
       len--;
@@ -270,6 +262,7 @@ public final class FrenchLightStemmer implements Stemmer, 
StemmerFactory {
 
       char ch = s[0];
       for (int i = 1; i < len; i++) {
+        // Character.isLetter is intentional: it is the letter test of the 
ported algorithm.
         if (s[i] == ch && Character.isLetter(ch)) len = delete(s, i--, len);
         else ch = s[i];
       }
@@ -281,6 +274,7 @@ public final class FrenchLightStemmer implements Stemmer, 
StemmerFactory {
       if (s[len - 1] == 'r') len--;
       if (s[len - 1] == 'e') len--;
       if (s[len - 1] == 'e') len--;
+      // Character.isLetter is intentional: it is the letter test of the 
ported algorithm.
       if (s[len - 1] == s[len - 2] && Character.isLetter(s[len - 1])) len--;
     }
     return len;
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchMinimalStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchMinimalStemmer.java
index 88ddde778..14a593101 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchMinimalStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/FrenchMinimalStemmer.java
@@ -54,11 +54,14 @@ package opennlp.tools.stemmer.light;
 import opennlp.tools.commons.ThreadSafe;
 import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
+
 /**
  * Light Stemmer for French.
  *
- * <p>This stemmer implements the following algorithm: <i>A Stemming procedure 
and stopword list for
- * general French corpora.</i> Jacques Savoy.
+ * <p>This stemmer implements the following algorithm:
+ * <a 
href="https://doi.org/10.1002/(SICI)1097-4571(1999)50:10%3C944::AID-ASI9%3E3.0.CO;2-Q">
+ * <i>A Stemming Procedure and Stopword List for General French 
Corpora</i></a> by Jacques Savoy
+ * (JASIS 50(10), 1999).
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -66,29 +69,17 @@ import opennlp.tools.stemmer.StemmerFactory;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class FrenchMinimalStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class FrenchMinimalStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len < 6) return len;
 
     if (s[len - 1] == 'x') {
@@ -100,6 +91,7 @@ public final class FrenchMinimalStemmer implements Stemmer, 
StemmerFactory {
     if (s[len - 1] == 'r') len--;
     if (s[len - 1] == 'e') len--;
     if (s[len - 1] == 'é') len--;
+    // Character.isLetter is intentional: it is the letter test of the ported 
algorithm.
     if (s[len - 1] == s[len - 2] && Character.isLetter(s[len - 1])) len--;
     return len;
   }
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanLightStemmer.java
index e1daca454..f0b81f04c 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanLightStemmer.java
@@ -54,11 +54,13 @@ package opennlp.tools.stemmer.light;
 import opennlp.tools.commons.ThreadSafe;
 import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
+
 /**
  * Light Stemmer for German.
  *
- * <p>This stemmer implements the "UniNE" algorithm in: <i>Light Stemming 
Approaches for the French,
- * Portuguese, German and Hungarian Languages</i> Jacques Savoy
+ * <p>This stemmer implements the "UniNE" algorithm in:
+ * <a href="https://doi.org/10.1145/1141277.1141523";><i>Light Stemming 
Approaches for the French,
+ * Portuguese, German and Hungarian Languages</i></a> by Jacques Savoy (ACM 
SAC 2006).
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -66,30 +68,17 @@ import opennlp.tools.stemmer.StemmerFactory;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class GermanLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class GermanLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     for (int i = 0; i < len; i++)
       switch (s[i]) {
         case 'ä':
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanMinimalStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanMinimalStemmer.java
index fe711a2ed..d6c2db31f 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanMinimalStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/GermanMinimalStemmer.java
@@ -54,11 +54,13 @@ package opennlp.tools.stemmer.light;
 import opennlp.tools.commons.ThreadSafe;
 import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
+
 /**
  * Minimal Stemmer for German.
  *
- * <p>This stemmer implements the following algorithm: <i>Morphologie et 
recherche d'information</i>
- * Jacques Savoy.
+ * <p>This stemmer implements the following algorithm:
+ * <a 
href="http://members.unine.ch/jacques.savoy/clef/morpho.pdf";><i>Morphologie et 
recherche
+ * d'information</i></a> by Jacques Savoy (technical report).
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -66,30 +68,17 @@ import opennlp.tools.stemmer.StemmerFactory;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class GermanMinimalStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class GermanMinimalStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len < 5) return len;
 
     for (int i = 0; i < len; i++)
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/HungarianLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/HungarianLightStemmer.java
index cb8fed7c6..ad0b9a962 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/HungarianLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/HungarianLightStemmer.java
@@ -56,11 +56,16 @@ import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+import static opennlp.tools.stemmer.light.StemmerUtil.isVowel;
+
 /**
  * Light Stemmer for Hungarian.
  *
- * <p>This stemmer implements the "UniNE" algorithm in: <i>Light Stemming 
Approaches for the French,
- * Portuguese, German and Hungarian Languages</i> Jacques Savoy
+ * <p>This stemmer implements the "UniNE" algorithm in:
+ * <a href="https://doi.org/10.1145/1141277.1141523";><i>Light Stemming 
Approaches for the French,
+ * Portuguese, German and Hungarian Languages</i></a> by Jacques Savoy (ACM 
SAC 2006). The inline
+ * suffix literals mirror the suffix tables of the cited paper and are kept 
inline for fidelity
+ * to the published algorithm.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -68,29 +73,17 @@ import static 
opennlp.tools.stemmer.light.StemmerUtil.endsWith;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class HungarianLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class HungarianLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     for (int i = 0; i < len; i++)
       switch (s[i]) {
         case 'á':
@@ -256,18 +249,4 @@ public final class HungarianLightStemmer implements 
Stemmer, StemmerFactory {
       }
     return len;
   }
-
-  private boolean isVowel(char ch) {
-    switch (ch) {
-      case 'a':
-      case 'e':
-      case 'i':
-      case 'o':
-      case 'u':
-      case 'y':
-        return true;
-      default:
-        return false;
-    }
-  }
 }
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/ItalianLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/ItalianLightStemmer.java
index 2664775df..23bee8ed2 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/ItalianLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/ItalianLightStemmer.java
@@ -54,11 +54,13 @@ package opennlp.tools.stemmer.light;
 import opennlp.tools.commons.ThreadSafe;
 import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
+
 /**
  * Light Stemmer for Italian.
  *
- * <p>This stemmer implements the algorithm described in: <i>Report on 
CLEF-2001 Experiments</i>
- * Jacques Savoy
+ * <p>This stemmer implements the algorithm described in:
+ * <a href="https://doi.org/10.1007/3-540-45691-0_3";><i>Report on CLEF-2001 
Experiments</i></a>
+ * by Jacques Savoy (CLEF 2001, LNCS 2406).
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -66,30 +68,17 @@ import opennlp.tools.stemmer.StemmerFactory;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class ItalianLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class ItalianLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len < 6) return len;
 
     for (int i = 0; i < len; i++)
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianLightStemmer.java
index 4caeb4815..51b49a483 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianLightStemmer.java
@@ -56,40 +56,22 @@ import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+
 /**
  * Light Stemmer for Norwegian.
  *
- * <p>Parts of this stemmer is adapted from SwedishLightStemFilter, except 
that while the Swedish
- * one has a pre-defined rule set and a corresponding corpus to validate 
against whereas the
+ * <p>Parts of this stemmer are adapted from the {@link SwedishLightStemmer}; 
while the Swedish
+ * algorithm has a pre-defined rule set and a corresponding corpus to validate 
against, the
  * Norwegian one is hand crafted.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
- * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
+ * Instances are immutable and safe for concurrent use by multiple threads; 
each instance is also
  * its own {@link StemmerFactory}. Input is expected to be lowercase, as 
produced by a
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class NorwegianLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
-  /** {@inheritDoc} */
-  @Override
-  public Stemmer newStemmer() {
-    return this;
-  }
+public final class NorwegianLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
 
   private final boolean useBokmaal;
   private final boolean useNynorsk;
@@ -118,8 +100,16 @@ public final class NorwegianLightStemmer implements 
Stemmer, StemmerFactory {
     useNynorsk = nynorsk;
   }
 
-  private int stem(char[] s, int len) {
-    // Remove posessive -s (bilens -> bilen) and continue checking
+  /** {@inheritDoc} */
+  @Override
+  public Stemmer newStemmer() {
+    return this;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
+    // Remove possessive -s (bilens -> bilen) and continue checking
     if (len > 4 && s[len - 1] == 's') len--;
 
     // Remove common endings, single-pass
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianMinimalStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianMinimalStemmer.java
index e079491fd..766099df7 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianMinimalStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/NorwegianMinimalStemmer.java
@@ -58,37 +58,18 @@ import opennlp.tools.stemmer.StemmerFactory;
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
 
 /**
- * Minimal Stemmer for Norwegian Bokmål (no-nb) and Nynorsk (no-nn)
+ * Minimal Stemmer for Norwegian Bokmål ({@code nb}) and Nynorsk ({@code nn}).
  *
- * <p>Stems known plural forms for Norwegian nouns only, together with genitiv 
-s
+ * <p>Stems known plural forms for Norwegian nouns only, together with the 
genitive {@code -s}.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
- * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
+ * Instances are immutable and safe for concurrent use by multiple threads; 
each instance is also
  * its own {@link StemmerFactory}. Input is expected to be lowercase, as 
produced by a
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class NorwegianMinimalStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
-  /** {@inheritDoc} */
-  @Override
-  public Stemmer newStemmer() {
-    return this;
-  }
+public final class NorwegianMinimalStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
 
   private final boolean useBokmaal;
   private final boolean useNynorsk;
@@ -117,8 +98,16 @@ public final class NorwegianMinimalStemmer implements 
Stemmer, StemmerFactory {
     useNynorsk = nynorsk;
   }
 
-  private int stem(char[] s, int len) {
-    // Remove genitiv s
+  /** {@inheritDoc} */
+  @Override
+  public Stemmer newStemmer() {
+    return this;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
+    // Remove genitive s
     if (len > 4 && s[len - 1] == 's') len--;
 
     if (len > 5
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/PortugueseLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/PortugueseLightStemmer.java
index bf85bbb26..ba155ce75 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/PortugueseLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/PortugueseLightStemmer.java
@@ -56,11 +56,15 @@ import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+
 /**
  * Light Stemmer for Portuguese
  *
- * <p>This stemmer implements the "UniNE" algorithm in: <i>Light Stemming 
Approaches for the French,
- * Portuguese, German and Hungarian Languages</i> Jacques Savoy
+ * <p>This stemmer implements the "UniNE" algorithm in:
+ * <a href="https://doi.org/10.1145/1141277.1141523";><i>Light Stemming 
Approaches for the French,
+ * Portuguese, German and Hungarian Languages</i></a> by Jacques Savoy (ACM 
SAC 2006). The inline
+ * suffix literals mirror the suffix tables of the cited paper and are kept 
inline for fidelity
+ * to the published algorithm.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -68,29 +72,17 @@ import static 
opennlp.tools.stemmer.light.StemmerUtil.endsWith;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class PortugueseLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class PortugueseLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len < 4) return len;
 
     len = removeSuffix(s, len);
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/RussianLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/RussianLightStemmer.java
index 2089b00c2..2e9e2a55d 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/RussianLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/RussianLightStemmer.java
@@ -56,11 +56,15 @@ import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+
 /**
  * Light Stemmer for Russian.
  *
- * <p>This stemmer implements the following algorithm: <i>Indexing and 
Searching Strategies for the
- * Russian Language.</i> Ljiljana Dolamic and Jacques Savoy.
+ * <p>This stemmer implements the following algorithm:
+ * <a href="https://doi.org/10.1002/asi.21191";><i>Indexing and Searching 
Strategies for the
+ * Russian Language</i></a> by Ljiljana Dolamic and Jacques Savoy (JASIST 
60(12), 2009). The
+ * inline suffix literals mirror the suffix tables of the cited paper and are 
kept inline for
+ * fidelity to the published algorithm.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -68,29 +72,17 @@ import static 
opennlp.tools.stemmer.light.StemmerUtil.endsWith;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class RussianLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class RussianLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     len = removeCase(s, len);
     return normalize(s, len);
   }
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishLightStemmer.java
index 0645593da..bcde8199c 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishLightStemmer.java
@@ -54,11 +54,13 @@ package opennlp.tools.stemmer.light;
 import opennlp.tools.commons.ThreadSafe;
 import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
+
 /**
  * Light Stemmer for Spanish
  *
- * <p>This stemmer implements the algorithm described in: <i>Report on 
CLEF-2001 Experiments</i>
- * Jacques Savoy
+ * <p>This stemmer implements the algorithm described in:
+ * <a href="https://doi.org/10.1007/3-540-45691-0_3";><i>Report on CLEF-2001 
Experiments</i></a>
+ * by Jacques Savoy (CLEF 2001, LNCS 2406).
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -66,30 +68,17 @@ import opennlp.tools.stemmer.StemmerFactory;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class SpanishLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class SpanishLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len < 5) return len;
 
     for (int i = 0; i < len; i++)
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishMinimalStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishMinimalStemmer.java
index b14ad4996..0a4c94a5e 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishMinimalStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SpanishMinimalStemmer.java
@@ -20,39 +20,29 @@ package opennlp.tools.stemmer.light;
 import opennlp.tools.commons.ThreadSafe;
 import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
+
 /**
  * Minimal plural stemmer for Spanish.
  *
- * <p>This stemmer implements the "plurals" stemmer for spanish language.
+ * <p>This stemmer implements the "plurals" stemmer for the Spanish language.
  *
- * @deprecated Use {@link SpanishPluralStemmer} instead.
+ * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
+ * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
+ * its own {@link StemmerFactory}. Input is expected to be lowercase, as 
produced by a
+ * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
-@Deprecated
 @ThreadSafe
-public final class SpanishMinimalStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class SpanishMinimalStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len < 4 || s[len - 1] != 's') return len;
 
     for (int i = 0; i < len; i++)
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/StemmerUtil.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/StemmerUtil.java
index 4c0b3f804..8745edc27 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/StemmerUtil.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/StemmerUtil.java
@@ -26,27 +26,6 @@ final class StemmerUtil {
   private StemmerUtil() {
   }
 
-  /**
-   * Checks whether the buffer starts with a prefix.
-   *
-   * @param s      The input buffer.
-   * @param len    The filled length of the buffer.
-   * @param prefix The prefix to test.
-   * @return {@code true} if the buffer starts with {@code prefix}.
-   */
-  static boolean startsWith(char[] s, int len, String prefix) {
-    final int prefixLen = prefix.length();
-    if (prefixLen > len) {
-      return false;
-    }
-    for (int i = 0; i < prefixLen; i++) {
-      if (s[i] != prefix.charAt(i)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   /**
    * Checks whether the buffer ends with a suffix.
    *
@@ -68,27 +47,6 @@ final class StemmerUtil {
     return true;
   }
 
-  /**
-   * Checks whether the buffer ends with a suffix.
-   *
-   * @param s      The input buffer.
-   * @param len    The filled length of the buffer.
-   * @param suffix The suffix to test.
-   * @return {@code true} if the buffer ends with {@code suffix}.
-   */
-  static boolean endsWith(char[] s, int len, char[] suffix) {
-    final int suffixLen = suffix.length;
-    if (suffixLen > len) {
-      return false;
-    }
-    for (int i = suffixLen - 1; i >= 0; i--) {
-      if (s[len - (suffixLen - i)] != suffix[i]) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   /**
    * Deletes one character in place.
    *
@@ -105,19 +63,24 @@ final class StemmerUtil {
   }
 
   /**
-   * Deletes {@code nChars} characters in place.
+   * Checks whether a character is one of the unaccented vowel letters {@code 
a}, {@code e},
+   * {@code i}, {@code o}, {@code u} or {@code y}. This is the vowel test 
shared by the Finnish
+   * and Hungarian light stemming algorithms.
    *
-   * @param s      The input buffer.
-   * @param pos    The position of the first character to delete.
-   * @param len    The filled length of the buffer.
-   * @param nChars The number of characters to delete; {@code pos + nChars} 
must not exceed
-   *               {@code len}.
-   * @return The filled length after the deletion.
+   * @param ch The character to test.
+   * @return {@code true} if {@code ch} is one of the six vowel letters.
    */
-  static int deleteN(char[] s, int pos, int len, int nChars) {
-    if (pos + nChars < len) {
-      System.arraycopy(s, pos + nChars, s, pos, len - pos - nChars);
+  static boolean isVowel(char ch) {
+    switch (ch) {
+      case 'a':
+      case 'e':
+      case 'i':
+      case 'o':
+      case 'u':
+      case 'y':
+        return true;
+      default:
+        return false;
     }
-    return len - nChars;
   }
 }
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishLightStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishLightStemmer.java
index 8f835c1a2..eda81fe45 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishLightStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishLightStemmer.java
@@ -56,11 +56,15 @@ import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+
 /**
  * Light Stemmer for Swedish.
  *
- * <p>This stemmer implements the algorithm described in: <i>Report on 
CLEF-2003 Monolingual
- * Tracks</i> Jacques Savoy
+ * <p>This stemmer implements the algorithm described in:
+ * <a 
href="https://ceur-ws.org/Vol-1169/CLEF2003wn-adhoc-Savoy2003b.pdf";><i>Report 
on CLEF-2003
+ * Monolingual Tracks</i></a> by Jacques Savoy (CLEF 2003 working notes). The 
inline suffix
+ * literals mirror the suffix tables of the cited paper and are kept inline 
for fidelity to the
+ * published algorithm.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -68,29 +72,17 @@ import static 
opennlp.tools.stemmer.light.StemmerUtil.endsWith;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class SwedishLightStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class SwedishLightStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len > 4 && s[len - 1] == 's') len--;
 
     if (len > 7 && (endsWith(s, len, "elser") || endsWith(s, len, "heten"))) 
return len - 5;
diff --git 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishMinimalStemmer.java
 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishMinimalStemmer.java
index 1eb2f6454..f2b07ceee 100644
--- 
a/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishMinimalStemmer.java
+++ 
b/opennlp-core/opennlp-runtime/src/main/java/opennlp/tools/stemmer/light/SwedishMinimalStemmer.java
@@ -55,13 +55,12 @@ import opennlp.tools.stemmer.Stemmer;
 import opennlp.tools.stemmer.StemmerFactory;
 
 import static opennlp.tools.stemmer.light.StemmerUtil.endsWith;
+
 /**
- * Minimal Stemmer for Swedish. The algorithm is an adapted version of the 
SwedishLightStemmer, but
- * only stripping the most common plural suffixes for nouns: 
-ar/arne/arna/aren, -at, -er/erna, -et,
- * -or/orna, -en. We do not strip -an or -ans suffixes, since that would 
require a large dictionary
- * of exceptions.
- *
- * @since 9.0.0
+ * Minimal Stemmer for Swedish. The algorithm is an adapted version of the
+ * {@link SwedishLightStemmer}, but only stripping the most common plural 
suffixes for nouns:
+ * -ar/arne/arna/aren, -at, -er/erna, -et, -or/orna, -en. We do not strip -an 
or -ans suffixes,
+ * since that would require a large dictionary of exceptions.
  *
  * <p>Adapted from the identically named algorithm in Apache Lucene's 
analysis-common module.
  * Instances are stateless and safe for concurrent use by multiple threads; 
each instance is also
@@ -69,29 +68,17 @@ import static 
opennlp.tools.stemmer.light.StemmerUtil.endsWith;
  * case-folding normalization step; the stemmer does not fold case itself.</p>
  */
 @ThreadSafe
-public final class SwedishMinimalStemmer implements Stemmer, StemmerFactory {
-  /** {@inheritDoc} */
-  @Override
-  public CharSequence stem(CharSequence word) {
-    if (word == null) {
-      throw new IllegalArgumentException("The word must not be null.");
-    }
-    final int length = word.length();
-    final char[] buffer = new char[length];
-    for (int i = 0; i < length; i++) {
-      buffer[i] = word.charAt(i);
-    }
-    final int stemmedLength = stem(buffer, length);
-    return new String(buffer, 0, stemmedLength);
-  }
-
+public final class SwedishMinimalStemmer extends AbstractCharArrayStemmer
+    implements StemmerFactory {
   /** {@inheritDoc} */
   @Override
   public Stemmer newStemmer() {
     return this;
   }
 
-  private int stem(char[] s, int len) {
+  /** {@inheritDoc} */
+  @Override
+  int stem(char[] s, int len) {
     if (len > 4 && s[len - 1] == 's') len--;
 
     if (len > 6
diff --git 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/stemmer/light/LightStemmerParityTest.java
 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/stemmer/light/LightStemmerParityTest.java
index 5003b8fea..24452efd8 100644
--- 
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/stemmer/light/LightStemmerParityTest.java
+++ 
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/stemmer/light/LightStemmerParityTest.java
@@ -29,6 +29,7 @@ import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 
 import opennlp.tools.stemmer.Stemmer;
+import opennlp.tools.stemmer.StemmerFactory;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -90,9 +91,8 @@ class LightStemmerParityTest {
   @ParameterizedTest(name = "{0}")
   @MethodSource("fixtures")
   void testStemmerIsItsOwnFactory(String fixture, Stemmer stemmer) {
-    final opennlp.tools.stemmer.StemmerFactory factory =
-        (opennlp.tools.stemmer.StemmerFactory) stemmer;
+    final StemmerFactory factory = (StemmerFactory) stemmer;
     assertEquals(stemmer, factory.newStemmer(),
-        "a stateless stemmer returns itself from newStemmer()");
+        "the stemmer returns itself from newStemmer()");
   }
 }

Reply via email to