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

ggregory 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 3bd162585 [LANG-1729] NumberUtils.isParsable() returns true for full 
width Unicode digits for inputs with a decimal point
3bd162585 is described below

commit 3bd1625859000571b99b4193df1360ab18a33910
Author: Gary Gregory <[email protected]>
AuthorDate: Tue Dec 16 08:30:18 2025 -0500

    [LANG-1729] NumberUtils.isParsable() returns true for full width Unicode
    digits for inputs with a decimal point
---
 src/changes/changes.xml                            |  1 +
 .../org/apache/commons/lang3/math/NumberUtils.java | 63 +++++++++++++---------
 .../apache/commons/lang3/math/NumberUtilsTest.java |  7 ++-
 3 files changed, 43 insertions(+), 28 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9e18b8e80..442b9d442 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -77,6 +77,7 @@ The <action> type attribute can be add,update,fix,remove.
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Fix race condition in Fraction.hashCode().</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Fix race condition in Range.hashCode().</action>
     <action                   type="fix" dev="ggregory" due-to="Gary Gregory, 
Akshat_Agg">Document safer deserialization option in Javadoc for 
SerializationUtils.</action>
+    <action issue="LANG-1729" type="fix" dev="ggregory" due-to="Gary Gregory, 
Andrei Anischevici, Akshat_Agg">NumberUtils.isParsable() returns true for full 
width Unicode digits for inputs with a decimal point.</action>
     <!-- ADD -->
     <!-- UPDATE -->
     <action                   type="update" dev="ggregory" due-to="Gary 
Gregory, Dependabot">Bump org.apache.commons:commons-parent from 92 to 93 
#1498.</action>
diff --git a/src/main/java/org/apache/commons/lang3/math/NumberUtils.java 
b/src/main/java/org/apache/commons/lang3/math/NumberUtils.java
index 740a15544..3bfd0ece3 100644
--- a/src/main/java/org/apache/commons/lang3/math/NumberUtils.java
+++ b/src/main/java/org/apache/commons/lang3/math/NumberUtils.java
@@ -724,19 +724,52 @@ public static boolean isNumber(final String str) {
      * @since 3.4
      */
     public static boolean isParsable(final String str) {
-        if (StringUtils.isEmpty(str)) {
-            return false;
-        }
-        if (str.charAt(str.length() - 1) == '.') {
+        if (StringUtils.isEmpty(str) || str.charAt(str.length() - 1) == '.') {
             return false;
         }
         if (str.charAt(0) == '-') {
             if (str.length() == 1) {
                 return false;
             }
-            return withDecimalsParsing(str, 1);
+            return isParsableDecimal(str, 1);
         }
-        return withDecimalsParsing(str, 0);
+        return isParsableDecimal(str, 0);
+    }
+
+    /**
+     * Tests whether a number string is parsable as a decimal number or 
integer.
+     *
+     * <ul>
+     * <li>At most one decimal point is allowed.</li>
+     * <li>No signs, exponents or type qualifiers are allowed.</li>
+     * <li>Only ASCII digits are allowed if a decimal point is present.</li>
+     * </ul>
+     *
+     * @param str      the String to test.
+     * @param beginIdx the index to start checking from.
+     * @return {@code true} if the string is a parsable number.
+     */
+    private static boolean isParsableDecimal(final String str, final int 
beginIdx) {
+        // See 
https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-NonZeroDigit
+        int decimalPoints = 0;
+        boolean asciiNumeric = true;
+        for (int i = beginIdx; i < str.length(); i++) {
+            final char ch = str.charAt(i);
+            final boolean isDecimalPoint = ch == '.';
+            if (isDecimalPoint) {
+                decimalPoints++;
+            }
+            if (decimalPoints > 1 || !isDecimalPoint && 
!Character.isDigit(ch)) {
+                return false;
+            }
+            if (!isDecimalPoint) {
+                asciiNumeric &= CharUtils.isAsciiNumeric(ch);
+            }
+            if (decimalPoints > 0 && !asciiNumeric) {
+                return false;
+            }
+        }
+        return true;
     }
 
     private static boolean isSign(final char ch) {
@@ -1772,24 +1805,6 @@ private static void validateArray(final Object array) {
         Validate.isTrue(Array.getLength(array) != 0, "Array cannot be empty.");
     }
 
-    private static boolean withDecimalsParsing(final String str, final int 
beginIdx) {
-        int decimalPoints = 0;
-        for (int i = beginIdx; i < str.length(); i++) {
-            final char ch = str.charAt(i);
-            final boolean isDecimalPoint = ch == '.';
-            if (isDecimalPoint) {
-                decimalPoints++;
-            }
-            if (decimalPoints > 1) {
-                return false;
-            }
-            if (!isDecimalPoint && !Character.isDigit(ch)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
     /**
      * {@link NumberUtils} instances should NOT be constructed in standard 
programming. Instead, the class should be used as {@code 
NumberUtils.toInt("6");}.
      *
diff --git a/src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java
index 2b39007b6..3b8aae4e9 100644
--- a/src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java
@@ -1042,6 +1042,9 @@ void testIsParsableFullWidthUnicodeJDK8326627() {
         final String fullWidth123 = "\uFF10\uFF11\uFF12";
         assertThrows(NumberFormatException.class, () -> 
Double.parseDouble(fullWidth123));
         assertThrows(NumberFormatException.class, () -> 
Float.parseFloat(fullWidth123));
+        assertTrue(NumberUtils.isParsable(fullWidth123));
+        assertFalse(NumberUtils.isParsable(fullWidth123 + ".0"));
+        assertFalse(NumberUtils.isParsable("0." + fullWidth123));
     }
 
     @Test
@@ -1075,8 +1078,6 @@ void testLang1729IsParsableByte() {
     void testLang1729IsParsableDouble() {
         assertTrue(isParsableDouble("1"));
         assertFalse(isParsableDouble("1 2 3"));
-        // TODO Expected to be fixed in Java 23
-        // assertTrue(isParsableDouble("123"));
         assertFalse(isParsableDouble("1 2 3"));
     }
 
@@ -1084,8 +1085,6 @@ void testLang1729IsParsableDouble() {
     void testLang1729IsParsableFloat() {
         assertTrue(isParsableFloat("1"));
         assertFalse(isParsableFloat("1 2 3"));
-        // TODO Expected to be fixed in Java 23
-        // assertTrue(isParsableFloat("123"));
         assertFalse(isParsableFloat("1 2 3"));
     }
 

Reply via email to