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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 9c86ffe30 Clarify StringUtils.normalizeSpace contract and preserve 
NBSP handling
9c86ffe30 is described below

commit 9c86ffe308e7c682df3b1cf76f27dfc5bb6a2243
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Oct 16 17:19:23 2026 -0400

    Clarify StringUtils.normalizeSpace contract and preserve NBSP handling
    
    Document XPath normalize-space as the inspiration rather than an exact
    behavioral contract. Explain Character.isWhitespace semantics, explicit
    U+00A0 conversion, preservation of other non-breaking spaces, and final
    trimming. Clarify that adjacent converted NBSPs are not collapsed.
    
    The method was introduced for LANG-640 in commit 823f2603a. LANG-910
    added NBSP support to the regex implementation in f9b486a17, released
    in Commons Lang 3.2. LANG-1020 introduced the current manual whitespace
    normalization and per-character NBSP conversion in bc8e23808 (PR #27).
    LANG-1226 restored final trimming in 7e85d1cf5 (PR #150).
    
    PR #1809 proposes removing NBSP conversion. Preserve the established
    behavior here and resolve its mismatch with the old Javadoc explicitly.
    Add regression tests for standalone, boundary, repeated, and mixed
    NBSPs, other non-breaking spaces, and Unicode whitespace.
    
    Validation: three focused tests passed; Javadoc generation with full
    doclint and git diff --check passed.
    
    References:
    https://issues.apache.org/jira/browse/LANG-640
    https://issues.apache.org/jira/browse/LANG-910
    https://issues.apache.org/jira/browse/LANG-1020
    https://issues.apache.org/jira/browse/LANG-1226
    https://github.com/apache/commons-lang/pull/27
    https://github.com/apache/commons-lang/pull/150
    https://github.com/apache/commons-lang/pull/1809
---
 .../java/org/apache/commons/lang3/StringUtils.java | 42 ++++++---------
 .../org/apache/commons/lang3/StringUtilsTest.java  | 62 ++++++++++++++--------
 2 files changed, 57 insertions(+), 47 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java 
b/src/main/java/org/apache/commons/lang3/StringUtils.java
index aaf30c3cc..832a063d1 100644
--- a/src/main/java/org/apache/commons/lang3/StringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringUtils.java
@@ -5484,37 +5484,27 @@ public static String mid(final String str, int pos, 
final int len) {
     }
 
     /**
-     * Similar to <a 
href="https://www.w3.org/TR/xpath/#function-normalize-space";>https://www.w3.org/TR/xpath/#function-normalize
 -space</a>
+     * Normalizes whitespace in a String.
      *
-     * <p>
-     * This function returns the argument string with whitespace normalized by 
using {@code {@link #trim(String)}} to remove leading and trailing whitespace 
and
-     * then replacing sequences of whitespace characters by a single space.
-     * </p>
-     * In XML, whitespace characters are the same as those allowed by the <a 
href="https://www.w3.org/TR/REC-xml/#NT-S";>S</a> production, which is S ::= 
(#x20 |
-     * #x9 | #xD | #xA)+
-     * <p>
-     * Java's regexp pattern \s defines whitespace as [ \t\n\x0B\f\r]
-     * </p>
-     * <p>
-     * For reference:
-     * </p>
-     * <ul>
-     * <li>\x0B = vertical tab</li>
-     * <li>\f = #xC = form feed</li>
-     * <li>#x20 = space</li>
-     * <li>#x9 = \t</li>
-     * <li>#xA = \n</li>
-     * <li>#xD = \r</li>
-     * </ul>
+     * <p>Removes leading and trailing whitespace and replaces each remaining 
sequence of characters recognized by {@link Character#isWhitespace(char)}
+     * with a single ordinary space ({@code U+0020}). This includes Unicode 
whitespace beyond the characters matched by the default regular expression
+     * {@code \s}, such as EM SPACE ({@code U+2003}).</p>
      *
-     * <p>
-     * The difference is that Java's whitespace includes vertical tab and form 
feed, which this function will also normalize. Additionally {@code {@link
-     * #trim(String)}} removes control characters (char &lt;= 32) from both 
ends of this String.
-     * </p>
+     * <p>Additionally, converts each non-breaking space ({@code U+00A0}) to 
an ordinary space, even though {@link Character#isWhitespace(char)} excludes it.
+     * These conversions do not collapse adjacent spaces: for example, 
<code>"a&#92;u00A0&#92;u00A0b"</code> becomes {@code "a  b"}. Finally,
+     * {@link String#trim()} removes
+     * leading and trailing characters whose values are at most {@code 
U+0020}, including converted non-breaking spaces. Other non-breaking spaces, 
such as
+     * FIGURE SPACE ({@code U+2007}) and NARROW NO-BREAK SPACE ({@code 
U+202F}), are preserved.</p>
+     *
+     * <p>This method was inspired by XPath's
+     * <a 
href="https://www.w3.org/TR/xpath/#function-normalize-space";>normalize-space</a>
 function, but does not reproduce its behavior.
+     * XPath normalizes only XML
+     * whitespace ({@code U+0020}, {@code U+0009}, {@code U+000D}, and {@code 
U+000A}) and preserves non-breaking spaces. This method additionally normalizes
+     * Java whitespace, converts {@code U+00A0}, and trims leading and 
trailing control characters as described above.</p>
      *
      * @param str The source String to normalize whitespaces from, may be null.
      * @return The modified string with whitespace normalized, {@code null} if 
null String input.
-     * @see Pattern
+     * @see Character#isWhitespace(char)
      * @see #trim(String)
      * @see <a 
href="https://www.w3.org/TR/xpath/#function-normalize-space";>https://www.w3.org/TR/xpath/#function-normalize-space</a>
      * @since 3.0
diff --git a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
index df4bae511..21e59b617 100644
--- a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java
@@ -141,6 +141,26 @@ public String toString() {
     private static final boolean[] ARRAY_FALSE_TRUE = {false, true};
     private static final boolean[] ARRAY_FALSE_TRUE_FALSE = {false, true, 
false};
 
+    private static void assertNoLoneSurrogates(final String result) {
+        for (int i = 0; i < result.length(); i++) {
+            final char ch = result.charAt(i);
+            if (Character.isHighSurrogate(ch)) {
+                assertTrue(i + 1 < result.length() && 
Character.isLowSurrogate(result.charAt(i + 1)),
+                        "lone high surrogate in: " + result);
+                i++; // skip the paired low surrogate
+            } else {
+                assertFalse(Character.isLowSurrogate(ch), "lone low surrogate 
in: " + result);
+            }
+        }
+    }
+
+    static Stream<TriFunction<String, Integer, String, String>> 
testPadSurrogatePair() {
+        return Stream.of(
+                StringUtils::center,
+                StringUtils::leftPad,
+                StringUtils::rightPad);
+    }
+
     private void innerTestSplit(final char separator, final String sepStr, 
final char noMatch) {
         final String msg = "Failed on separator hex(" + 
Integer.toHexString(separator) +
                 "), noMatch hex(" + Integer.toHexString(noMatch) + "), 
sepStr(" + sepStr + ")";
@@ -226,19 +246,6 @@ private void innerTestSplitPreserveAllTokens(final char 
separator, final String
         assertEquals(str.substring(2), res[1], msg);
     }
 
-    private static void assertNoLoneSurrogates(final String result) {
-        for (int i = 0; i < result.length(); i++) {
-            final char ch = result.charAt(i);
-            if (Character.isHighSurrogate(ch)) {
-                assertTrue(i + 1 < result.length() && 
Character.isLowSurrogate(result.charAt(i + 1)),
-                        "lone high surrogate in: " + result);
-                i++; // skip the paired low surrogate
-            } else {
-                assertFalse(Character.isLowSurrogate(ch), "lone low surrogate 
in: " + result);
-            }
-        }
-    }
-
     /**
      * Tests {@code appendIfMissing}.
      */
@@ -1444,7 +1451,7 @@ void testLowerCase() {
 
     @Test
     void testNormalizeSpace() {
-        // Java says a non-breaking whitespace is not a whitespace.
+        // Character.isWhitespace excludes non-breaking space; normalizeSpace 
converts it explicitly.
         assertFalse(Character.isWhitespace('\u00A0'));
         //
         assertNull(StringUtils.normalizeSpace(null));
@@ -1469,6 +1476,26 @@ void testNormalizeSpace() {
         assertEquals("b", StringUtils.normalizeSpace("b\u0000"));
     }
 
+    @Test
+    void testNormalizeSpaceNonBreakingSpace() {
+        assertEquals("", StringUtils.normalizeSpace("\u00A0"));
+        assertEquals("", StringUtils.normalizeSpace("\u00A0\u00A0"));
+        assertEquals("a", StringUtils.normalizeSpace("\u00A0a"));
+        assertEquals("a", StringUtils.normalizeSpace("a\u00A0"));
+        assertEquals("a", StringUtils.normalizeSpace(" \u00A0a\u00A0 "));
+        assertEquals("a b", StringUtils.normalizeSpace("a\u00A0b"));
+        assertEquals("a  b", StringUtils.normalizeSpace("a\u00A0\u00A0b"));
+        assertEquals("a  b", StringUtils.normalizeSpace("a\u00A0 b"));
+        assertEquals("a  b", StringUtils.normalizeSpace("a \u00A0b"));
+        assertEquals("\u2007a\u2007", 
StringUtils.normalizeSpace("\u2007a\u2007"));
+        assertEquals("\u202Fa\u202F", 
StringUtils.normalizeSpace("\u202Fa\u202F"));
+    }
+
+    @Test
+    void testNormalizeSpaceUnicodeWhitespace() {
+        assertEquals("a b", 
StringUtils.normalizeSpace("\u2003a\u2003\u2003b\u2003"));
+    }
+
     @Test
     void testOverlay_StringStringIntInt() {
         assertNull(StringUtils.overlay(null, null, 2, 4));
@@ -1541,13 +1568,6 @@ void testPadMixedSurrogatePair() {
         assertEquals("a" + grin + "xa" + grin, StringUtils.center("x", 7, 
pad));
     }
 
-    static Stream<TriFunction<String, Integer, String, String>> 
testPadSurrogatePair() {
-        return Stream.of(
-                StringUtils::center,
-                StringUtils::leftPad,
-                StringUtils::rightPad);
-    }
-
     @ParameterizedTest
     @MethodSource
     void testPadSurrogatePair(final TriFunction<String, Integer, String, 
String> padFunction) {

Reply via email to