Skip to content

Commit 8089a0c

Browse files
authored
[LANG-1806] NumberUtils.isParsable("1.f") should return true (#1560)
* Add testLang1641() * Rename some test methods * [LANG-1806] NumberUtils.isParsable("1.f") should return true - Return true for numbers like 1.2e-5d and 1.2e-5f - Use a regular expression * [LANG-1806] NumberUtils.isParsable("1.f") should return true - Return true for numbers like 1.2e-5d and 1.2e-5f - Return true when one of the following would work: -- Double.parseDouble(String) -- Float.parseFloat(String) -- Long.parseLong(String) -- Integer.parseInteger(String) There are so many cases that it's simpler to try to parse and catch exceptions, instead of re-creating all the parsing rules from the JRE. The only downside is that number instances are created and discarded.
1 parent ba6cf8e commit 8089a0c

2 files changed

Lines changed: 134 additions & 82 deletions

File tree

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

Lines changed: 18 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.math.BigInteger;
2222
import java.math.RoundingMode;
2323
import java.util.Objects;
24+
import java.util.function.Consumer;
2425

2526
import org.apache.commons.lang3.CharUtils;
2627
import org.apache.commons.lang3.StringUtils;
@@ -104,6 +105,15 @@ public class NumberUtils {
104105
*/
105106
public static final Long LONG_INT_MIN_VALUE = Long.valueOf(Integer.MIN_VALUE);
106107

108+
private static <T> boolean accept(final Consumer<T> consumer, final T obj) {
109+
try {
110+
consumer.accept(obj);
111+
return true;
112+
} catch (Exception e) {
113+
return false;
114+
}
115+
}
116+
107117
/**
108118
* Compares two {@code byte} values numerically. This is the same functionality as provided in Java 7.
109119
*
@@ -167,7 +177,7 @@ public static int compare(final short x, final short y) {
167177
* Returns {@code null} if the string is {@code null}.
168178
* </p>
169179
*
170-
* @param str a {@link String} to convert, may be null.
180+
* @param str a {@link String} to convert, may be null.Return
171181
* @return converted {@link BigDecimal} (or null if the input is null).
172182
* @throws NumberFormatException if the value cannot be converted.
173183
*/
@@ -730,7 +740,8 @@ public static boolean isNumber(final String str) {
730740
* </p>
731741
*
732742
* <p>
733-
* Hexadecimal and scientific notations are <strong>not</strong> considered parsable. See {@link #isCreatable(String)} on those cases.
743+
* Scientific notation (for example, {@code "1.2e-5"}) and type suffixes (e.g., {@code "2.0f"}, {@code "2.0d"}) are supported
744+
* as they are valid for {@link Float#parseFloat(String)} and {@link Double#parseDouble(String)}.
734745
* </p>
735746
*
736747
* <p>
@@ -739,55 +750,14 @@ public static boolean isNumber(final String str) {
739750
*
740751
* @param str the String to check.
741752
* @return {@code true} if the string is a parsable number.
753+
* @see Integer#parseInt(String)
754+
* @see Long#parseLong(String)
755+
* @see Double#parseDouble(String)
756+
* @see Float#parseFloat(String)
742757
* @since 3.4
743758
*/
744759
public static boolean isParsable(final String str) {
745-
if (StringUtils.isEmpty(str)) {
746-
return false;
747-
}
748-
if (str.charAt(0) == '-') {
749-
if (str.length() == 1) {
750-
return false;
751-
}
752-
return isParsableDecimal(str, 1);
753-
}
754-
return isParsableDecimal(str, 0);
755-
}
756-
757-
/**
758-
* Tests whether a number string is parsable as a decimal number or integer.
759-
*
760-
* <ul>
761-
* <li>At most one decimal point is allowed.</li>
762-
* <li>No signs, exponents or type qualifiers are allowed.</li>
763-
* <li>Only ASCII digits are allowed if a decimal point is present.</li>
764-
* </ul>
765-
*
766-
* @param str the String to test.
767-
* @param beginIdx the index to start checking from.
768-
* @return {@code true} if the string is a parsable number.
769-
*/
770-
private static boolean isParsableDecimal(final String str, final int beginIdx) {
771-
// See https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-NonZeroDigit
772-
int decimalPoints = 0;
773-
boolean asciiNumeric = true;
774-
for (int i = beginIdx; i < str.length(); i++) {
775-
final char ch = str.charAt(i);
776-
final boolean isDecimalPoint = ch == '.';
777-
if (isDecimalPoint) {
778-
decimalPoints++;
779-
}
780-
if (decimalPoints > 1 || !isDecimalPoint && !Character.isDigit(ch)) {
781-
return false;
782-
}
783-
if (!isDecimalPoint) {
784-
asciiNumeric &= CharUtils.isAsciiNumeric(ch);
785-
}
786-
if (decimalPoints > 0 && !asciiNumeric) {
787-
return false;
788-
}
789-
}
790-
return true;
760+
return accept(Double::parseDouble, str) || accept(Long::parseLong, str) || accept(Float::parseFloat, str) || accept(Long::parseLong, str);
791761
}
792762

793763
private static boolean isSign(final char ch) {

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

Lines changed: 116 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
import org.apache.commons.lang3.AbstractLangTest;
3939
import org.junit.jupiter.api.Test;
40+
import org.junit.jupiter.params.ParameterizedTest;
41+
import org.junit.jupiter.params.provider.ValueSource;
4042

4143
/**
4244
* Tests {@link org.apache.commons.lang3.math.NumberUtils}.
@@ -1004,36 +1006,83 @@ void testIsNumberLANG992() {
10041006
compareIsNumberWithCreateNumber("0.4790", true);
10051007
}
10061008

1007-
@Test
1008-
void testIsParsable() {
1009-
assertFalse(NumberUtils.isParsable(null));
1010-
assertFalse(NumberUtils.isParsable(""));
1011-
assertFalse(NumberUtils.isParsable("0xC1AB"));
1012-
assertFalse(NumberUtils.isParsable("65CBA2"));
1013-
assertFalse(NumberUtils.isParsable("pendro"));
1014-
assertFalse(NumberUtils.isParsable("64, 2"));
1015-
assertFalse(NumberUtils.isParsable("64.2.2"));
1016-
assertFalse(NumberUtils.isParsable("64.."));
1017-
assertTrue(NumberUtils.isParsable("64."));
1018-
assertTrue(NumberUtils.isParsable("-64."));
1019-
assertFalse(NumberUtils.isParsable("64L"));
1020-
assertFalse(NumberUtils.isParsable("-"));
1021-
assertFalse(NumberUtils.isParsable("--2"));
1022-
assertTrue(NumberUtils.isParsable("64.2"));
1023-
assertTrue(NumberUtils.isParsable("64"));
1024-
assertTrue(NumberUtils.isParsable("018"));
1025-
assertTrue(NumberUtils.isParsable(".18"));
1026-
assertTrue(NumberUtils.isParsable("-65"));
1027-
assertTrue(NumberUtils.isParsable("-018"));
1028-
assertTrue(NumberUtils.isParsable("-018.2"));
1029-
assertTrue(NumberUtils.isParsable("-.236"));
1030-
assertTrue(NumberUtils.isParsable("2."));
1031-
// TODO assertTrue(NumberUtils.isParsable("2.f"));
1032-
// TODO assertTrue(NumberUtils.isParsable("2.d"));
1033-
// Float.parseFloat("1.2e-5f")
1034-
// TODO assertTrue(NumberUtils.isParsable("1.2e-5f"));
1035-
// Double.parseDouble("1.2e-5d")
1036-
// TODO assertTrue(NumberUtils.isParsable("1.2e-5d"));
1009+
@ParameterizedTest
1010+
// @formatter:off
1011+
@ValueSource(strings = {
1012+
// Decimal floating-point literals (no suffix or 'd'/'D' suffix)
1013+
"3.14",
1014+
"3.14d",
1015+
"3.14D",
1016+
".5",
1017+
".5d",
1018+
"5.",
1019+
"5.d",
1020+
"5d",
1021+
"0.0",
1022+
"0.0d",
1023+
// Exponential (scientific) notation
1024+
"1.23e10",
1025+
"1.23e10d",
1026+
"1.23E10",
1027+
"1.23e-10",
1028+
"1.23e-10d",
1029+
"1e5",
1030+
"1e5d",
1031+
".5e3",
1032+
".5e3d",
1033+
// Hexadecimal floating-point literals
1034+
"0x1.8p3",
1035+
"0x1.8p3d",
1036+
"0x.8p0",
1037+
"0x1p-3",
1038+
"0x1.fffffffffffffp1023",
1039+
"0x1.fffffffffffffp1023d",
1040+
// With leading zeros
1041+
"01.5",
1042+
"01.5d" })
1043+
// @formatter:on
1044+
void testIsParsableDoubleTrue(final String input) {
1045+
Double.parseDouble(input);
1046+
assertTrue(NumberUtils.isParsable(input));
1047+
}
1048+
1049+
@ParameterizedTest
1050+
@ValueSource(strings = { "", "0xC1AB", "65CBA2", "pendro", "64, 2", "64.2.2", "64..", "64L", "-", "--2",
1051+
// Invalid scientific notation.
1052+
"e5", "1e", "1e+", "1e-", "1ee5", "1e5e5",
1053+
// Invalid type suffixes.
1054+
"f", "d", "-f", "-d", })
1055+
void testIsParsableFalse(final String input) {
1056+
assertFalse(NumberUtils.isParsable(input));
1057+
}
1058+
1059+
@ParameterizedTest
1060+
// @formatter:off
1061+
@ValueSource(strings = {
1062+
// Decimal floating-point literals
1063+
"3.14f",
1064+
"3.14F",
1065+
".5f",
1066+
"5.f",
1067+
"5f",
1068+
"0.0f",
1069+
// Exponential (scientific) notation
1070+
"1.23e10f",
1071+
"1.23E10f",
1072+
"1.23e-10f",
1073+
"1e5f",
1074+
".5e3f",
1075+
// Hexadecimal floating-point literals
1076+
"0x1.8p3f",
1077+
"0x.8p0f",
1078+
"0x1p-3f",
1079+
"0x1.fffffep127f",
1080+
// With leading zeros
1081+
"01.5f"})
1082+
// @formatter:on
1083+
void testIsParsableFloatTrue(final String input) {
1084+
Float.parseFloat(input);
1085+
assertTrue(NumberUtils.isParsable(input));
10371086
}
10381087

10391088
/**
@@ -1056,6 +1105,39 @@ void testIsParsableFullWidthUnicodeJDK8326627() {
10561105
assertFalse(NumberUtils.isParsable("0." + fullWidth123));
10571106
}
10581107

1108+
@Test
1109+
void testIsParsableNull() {
1110+
// Can't use null in @ValueSource(strings)
1111+
assertFalse(NumberUtils.isParsable(null));
1112+
}
1113+
1114+
@ParameterizedTest
1115+
// @formatter:off
1116+
@ValueSource(strings = {
1117+
"64.",
1118+
"-64.",
1119+
"64.2",
1120+
"64",
1121+
"018",
1122+
".18",
1123+
"-65",
1124+
"-018",
1125+
"-018.2",
1126+
"-.236",
1127+
"2.",
1128+
"2.f",
1129+
"2.d",
1130+
"1.2e-5f",
1131+
"1.2e-5d",
1132+
// Additional tests for scientific notation.
1133+
"1e5", "1E5", "1.2e5", "1.2E5", "1.2e+5", "1.2e-5", "-1.2e-5", "1e5f", "1e5F", "1e5d", "1e5D",
1134+
// Additional tests for type suffixes.
1135+
"2f", "2F", "2d", "2D", "2.0f", "2.0F", "2.0d", "2.0D", "-2.0f", "-2.0d" })
1136+
// @formatter:on
1137+
void testIsParsableTrue(final String input) {
1138+
assertTrue(NumberUtils.isParsable(input));
1139+
}
1140+
10591141
@Test
10601142
void testLang1087() {
10611143
// no sign cases
@@ -1087,8 +1169,8 @@ void testLang1729IsParsableByte() {
10871169
void testLang1729IsParsableDouble() {
10881170
assertTrue(isParsableDouble("1"));
10891171
assertTrue(isParsableDouble("1."));
1090-
// TODO assertTrue(isParsableDouble("1.f"));
1091-
// TODO assertTrue(isParsableDouble("1.d"));
1172+
assertTrue(isParsableDouble("1.f"));
1173+
assertTrue(isParsableDouble("1.d"));
10921174
assertTrue(isParsableDouble("1.0"));
10931175
assertFalse(isParsableDouble("1.0."));
10941176
assertFalse(isParsableDouble("1 2 3"));
@@ -1099,8 +1181,8 @@ void testLang1729IsParsableDouble() {
10991181
void testLang1729IsParsableFloat() {
11001182
assertTrue(isParsableFloat("1"));
11011183
assertTrue(isParsableFloat("1."));
1102-
// TODO assertTrue(isParsableFloat("1.f"));
1103-
// TODO assertTrue(isParsableFloat("1.d"));
1184+
assertTrue(isParsableFloat("1.f"));
1185+
assertTrue(isParsableFloat("1.d"));
11041186
assertTrue(isParsableFloat("1.0"));
11051187
assertFalse(isParsableFloat("1.0."));
11061188
assertFalse(isParsableFloat("1 2 3"));

0 commit comments

Comments
 (0)