Skip to content

Commit 3bd1625

Browse files
committed
[LANG-1729] NumberUtils.isParsable() returns true for full width Unicode
digits for inputs with a decimal point
1 parent 1196f03 commit 3bd1625

File tree

3 files changed

+43
-28
lines changed

3 files changed

+43
-28
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ The <action> type attribute can be add,update,fix,remove.
7777
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix race condition in Fraction.hashCode().</action>
7878
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix race condition in Range.hashCode().</action>
7979
<action type="fix" dev="ggregory" due-to="Gary Gregory, Akshat_Agg">Document safer deserialization option in Javadoc for SerializationUtils.</action>
80+
<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>
8081
<!-- ADD -->
8182
<!-- UPDATE -->
8283
<action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 92 to 93 #1498.</action>

src/main/java/org/apache/commons/lang3/math/NumberUtils.java

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -724,19 +724,52 @@ public static boolean isNumber(final String str) {
724724
* @since 3.4
725725
*/
726726
public static boolean isParsable(final String str) {
727-
if (StringUtils.isEmpty(str)) {
728-
return false;
729-
}
730-
if (str.charAt(str.length() - 1) == '.') {
727+
if (StringUtils.isEmpty(str) || str.charAt(str.length() - 1) == '.') {
731728
return false;
732729
}
733730
if (str.charAt(0) == '-') {
734731
if (str.length() == 1) {
735732
return false;
736733
}
737-
return withDecimalsParsing(str, 1);
734+
return isParsableDecimal(str, 1);
738735
}
739-
return withDecimalsParsing(str, 0);
736+
return isParsableDecimal(str, 0);
737+
}
738+
739+
/**
740+
* Tests whether a number string is parsable as a decimal number or integer.
741+
*
742+
* <ul>
743+
* <li>At most one decimal point is allowed.</li>
744+
* <li>No signs, exponents or type qualifiers are allowed.</li>
745+
* <li>Only ASCII digits are allowed if a decimal point is present.</li>
746+
* </ul>
747+
*
748+
* @param str the String to test.
749+
* @param beginIdx the index to start checking from.
750+
* @return {@code true} if the string is a parsable number.
751+
*/
752+
private static boolean isParsableDecimal(final String str, final int beginIdx) {
753+
// See https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-NonZeroDigit
754+
int decimalPoints = 0;
755+
boolean asciiNumeric = true;
756+
for (int i = beginIdx; i < str.length(); i++) {
757+
final char ch = str.charAt(i);
758+
final boolean isDecimalPoint = ch == '.';
759+
if (isDecimalPoint) {
760+
decimalPoints++;
761+
}
762+
if (decimalPoints > 1 || !isDecimalPoint && !Character.isDigit(ch)) {
763+
return false;
764+
}
765+
if (!isDecimalPoint) {
766+
asciiNumeric &= CharUtils.isAsciiNumeric(ch);
767+
}
768+
if (decimalPoints > 0 && !asciiNumeric) {
769+
return false;
770+
}
771+
}
772+
return true;
740773
}
741774

742775
private static boolean isSign(final char ch) {
@@ -1772,24 +1805,6 @@ private static void validateArray(final Object array) {
17721805
Validate.isTrue(Array.getLength(array) != 0, "Array cannot be empty.");
17731806
}
17741807

1775-
private static boolean withDecimalsParsing(final String str, final int beginIdx) {
1776-
int decimalPoints = 0;
1777-
for (int i = beginIdx; i < str.length(); i++) {
1778-
final char ch = str.charAt(i);
1779-
final boolean isDecimalPoint = ch == '.';
1780-
if (isDecimalPoint) {
1781-
decimalPoints++;
1782-
}
1783-
if (decimalPoints > 1) {
1784-
return false;
1785-
}
1786-
if (!isDecimalPoint && !Character.isDigit(ch)) {
1787-
return false;
1788-
}
1789-
}
1790-
return true;
1791-
}
1792-
17931808
/**
17941809
* {@link NumberUtils} instances should NOT be constructed in standard programming. Instead, the class should be used as {@code NumberUtils.toInt("6");}.
17951810
*

src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,9 @@ void testIsParsableFullWidthUnicodeJDK8326627() {
10421042
final String fullWidth123 = "\uFF10\uFF11\uFF12";
10431043
assertThrows(NumberFormatException.class, () -> Double.parseDouble(fullWidth123));
10441044
assertThrows(NumberFormatException.class, () -> Float.parseFloat(fullWidth123));
1045+
assertTrue(NumberUtils.isParsable(fullWidth123));
1046+
assertFalse(NumberUtils.isParsable(fullWidth123 + ".0"));
1047+
assertFalse(NumberUtils.isParsable("0." + fullWidth123));
10451048
}
10461049

10471050
@Test
@@ -1075,17 +1078,13 @@ void testLang1729IsParsableByte() {
10751078
void testLang1729IsParsableDouble() {
10761079
assertTrue(isParsableDouble("1"));
10771080
assertFalse(isParsableDouble("1 2 3"));
1078-
// TODO Expected to be fixed in Java 23
1079-
// assertTrue(isParsableDouble("123"));
10801081
assertFalse(isParsableDouble("1 2 3"));
10811082
}
10821083

10831084
@Test
10841085
void testLang1729IsParsableFloat() {
10851086
assertTrue(isParsableFloat("1"));
10861087
assertFalse(isParsableFloat("1 2 3"));
1087-
// TODO Expected to be fixed in Java 23
1088-
// assertTrue(isParsableFloat("123"));
10891088
assertFalse(isParsableFloat("1 2 3"));
10901089
}
10911090

0 commit comments

Comments
 (0)