From a5ffdf0a7fb7e5a748184128f5f4007f27267f91 Mon Sep 17 00:00:00 2001 From: Vijay Sai Date: Sat, 11 Jul 2026 18:00:53 +0530 Subject: [PATCH] Keep WordUtils.abbreviate from splitting a surrogate pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WordUtils.abbreviate() cuts the string at raw char offsets, so an upper limit that lands between a high surrogate and its trailing low surrogate leaves an unpaired surrogate at the end of the result. For example, WordUtils.abbreviate("😀😀😀", 0, 3, "") returned one emoji followed by a lone high surrogate instead of just the emoji. Back the cut off by one when it would split a pair, in both the no-space-found and the space-found branches, mirroring the existing handling in WordUtils.wrap() and StringUtils.abbreviate(). --- .../org/apache/commons/text/WordUtils.java | 23 +++++++++++++++++-- .../apache/commons/text/WordUtilsTest.java | 11 +++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/text/WordUtils.java b/src/main/java/org/apache/commons/text/WordUtils.java index 4b6ad0ea0e..198bbe71be 100644 --- a/src/main/java/org/apache/commons/text/WordUtils.java +++ b/src/main/java/org/apache/commons/text/WordUtils.java @@ -93,13 +93,21 @@ public static String abbreviate(final String str, int lower, int upper, final St final StringBuilder result = new StringBuilder(); final int index = Strings.CS.indexOf(str, " ", lower); if (index == -1) { - result.append(str, 0, upper); + int end = upper; + if (splitsSurrogatePair(str, end)) { + end--; + } + result.append(str, 0, end); // only if abbreviation has occurred do we append the appendToEnd value if (upper != str.length()) { result.append(StringUtils.defaultString(appendToEnd)); } } else { - result.append(str, 0, Math.min(index, upper)); + int end = Math.min(index, upper); + if (splitsSurrogatePair(str, end)) { + end--; + } + result.append(str, 0, end); result.append(StringUtils.defaultString(appendToEnd)); } return result.toString(); @@ -426,6 +434,17 @@ public static boolean isDelimiter(final int codePoint, final char[] delimiters) return false; } + /** + * Tests whether the given index splits a surrogate pair, that is, whether it falls between a high surrogate and its trailing low surrogate. + * + * @param str The String to check. + * @param index The index to test. + * @return Whether cutting {@code str} at {@code index} would split a surrogate pair. + */ + private static boolean splitsSurrogatePair(final String str, final int index) { + return index > 0 && index < str.length() && Character.isHighSurrogate(str.charAt(index - 1)) && Character.isLowSurrogate(str.charAt(index)); + } + /** * Swaps the case of a String using a word based algorithm. *