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"));
}