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 8089a0c2d [LANG-1806] NumberUtils.isParsable("1.f") should return true
(#1560)
8089a0c2d is described below
commit 8089a0c2d3207ed26531f877352336baf1c6c5bb
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Jan 11 17:29:51 2026 -0500
[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.
---
.../org/apache/commons/lang3/math/NumberUtils.java | 66 +++------
.../apache/commons/lang3/math/NumberUtilsTest.java | 150 ++++++++++++++++-----
2 files changed, 134 insertions(+), 82 deletions(-)
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 2ef56f4d9..6abd66502 100644
--- a/src/main/java/org/apache/commons/lang3/math/NumberUtils.java
+++ b/src/main/java/org/apache/commons/lang3/math/NumberUtils.java
@@ -21,6 +21,7 @@
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Objects;
+import java.util.function.Consumer;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
@@ -104,6 +105,15 @@ public class NumberUtils {
*/
public static final Long LONG_INT_MIN_VALUE =
Long.valueOf(Integer.MIN_VALUE);
+ private static <T> boolean accept(final Consumer<T> consumer, final T obj)
{
+ try {
+ consumer.accept(obj);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
/**
* Compares two {@code byte} values numerically. This is the same
functionality as provided in Java 7.
*
@@ -167,7 +177,7 @@ public static int compare(final short x, final short y) {
* Returns {@code null} if the string is {@code null}.
* </p>
*
- * @param str a {@link String} to convert, may be null.
+ * @param str a {@link String} to convert, may be null.Return
* @return converted {@link BigDecimal} (or null if the input is null).
* @throws NumberFormatException if the value cannot be converted.
*/
@@ -730,7 +740,8 @@ public static boolean isNumber(final String str) {
* </p>
*
* <p>
- * Hexadecimal and scientific notations are <strong>not</strong>
considered parsable. See {@link #isCreatable(String)} on those cases.
+ * Scientific notation (for example, {@code "1.2e-5"}) and type suffixes
(e.g., {@code "2.0f"}, {@code "2.0d"}) are supported
+ * as they are valid for {@link Float#parseFloat(String)} and {@link
Double#parseDouble(String)}.
* </p>
*
* <p>
@@ -739,55 +750,14 @@ public static boolean isNumber(final String str) {
*
* @param str the String to check.
* @return {@code true} if the string is a parsable number.
+ * @see Integer#parseInt(String)
+ * @see Long#parseLong(String)
+ * @see Double#parseDouble(String)
+ * @see Float#parseFloat(String)
* @since 3.4
*/
public static boolean isParsable(final String str) {
- if (StringUtils.isEmpty(str)) {
- return false;
- }
- if (str.charAt(0) == '-') {
- if (str.length() == 1) {
- return false;
- }
- return isParsableDecimal(str, 1);
- }
- 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;
+ return accept(Double::parseDouble, str) || accept(Long::parseLong,
str) || accept(Float::parseFloat, str) || accept(Long::parseLong, str);
}
private static boolean isSign(final char ch) {
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 528039c9c..105dda462 100644
--- a/src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java
@@ -37,6 +37,8 @@
import org.apache.commons.lang3.AbstractLangTest;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Tests {@link org.apache.commons.lang3.math.NumberUtils}.
@@ -1004,36 +1006,83 @@ void testIsNumberLANG992() {
compareIsNumberWithCreateNumber("0.4790", true);
}
- @Test
- void testIsParsable() {
- assertFalse(NumberUtils.isParsable(null));
- assertFalse(NumberUtils.isParsable(""));
- assertFalse(NumberUtils.isParsable("0xC1AB"));
- assertFalse(NumberUtils.isParsable("65CBA2"));
- assertFalse(NumberUtils.isParsable("pendro"));
- assertFalse(NumberUtils.isParsable("64, 2"));
- assertFalse(NumberUtils.isParsable("64.2.2"));
- assertFalse(NumberUtils.isParsable("64.."));
- assertTrue(NumberUtils.isParsable("64."));
- assertTrue(NumberUtils.isParsable("-64."));
- assertFalse(NumberUtils.isParsable("64L"));
- assertFalse(NumberUtils.isParsable("-"));
- assertFalse(NumberUtils.isParsable("--2"));
- assertTrue(NumberUtils.isParsable("64.2"));
- assertTrue(NumberUtils.isParsable("64"));
- assertTrue(NumberUtils.isParsable("018"));
- assertTrue(NumberUtils.isParsable(".18"));
- assertTrue(NumberUtils.isParsable("-65"));
- assertTrue(NumberUtils.isParsable("-018"));
- assertTrue(NumberUtils.isParsable("-018.2"));
- assertTrue(NumberUtils.isParsable("-.236"));
- assertTrue(NumberUtils.isParsable("2."));
- // TODO assertTrue(NumberUtils.isParsable("2.f"));
- // TODO assertTrue(NumberUtils.isParsable("2.d"));
- // Float.parseFloat("1.2e-5f")
- // TODO assertTrue(NumberUtils.isParsable("1.2e-5f"));
- // Double.parseDouble("1.2e-5d")
- // TODO assertTrue(NumberUtils.isParsable("1.2e-5d"));
+ @ParameterizedTest
+ // @formatter:off
+ @ValueSource(strings = {
+ // Decimal floating-point literals (no suffix or 'd'/'D' suffix)
+ "3.14",
+ "3.14d",
+ "3.14D",
+ ".5",
+ ".5d",
+ "5.",
+ "5.d",
+ "5d",
+ "0.0",
+ "0.0d",
+ // Exponential (scientific) notation
+ "1.23e10",
+ "1.23e10d",
+ "1.23E10",
+ "1.23e-10",
+ "1.23e-10d",
+ "1e5",
+ "1e5d",
+ ".5e3",
+ ".5e3d",
+ // Hexadecimal floating-point literals
+ "0x1.8p3",
+ "0x1.8p3d",
+ "0x.8p0",
+ "0x1p-3",
+ "0x1.fffffffffffffp1023",
+ "0x1.fffffffffffffp1023d",
+ // With leading zeros
+ "01.5",
+ "01.5d" })
+ // @formatter:on
+ void testIsParsableDoubleTrue(final String input) {
+ Double.parseDouble(input);
+ assertTrue(NumberUtils.isParsable(input));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "", "0xC1AB", "65CBA2", "pendro", "64, 2",
"64.2.2", "64..", "64L", "-", "--2",
+ // Invalid scientific notation.
+ "e5", "1e", "1e+", "1e-", "1ee5", "1e5e5",
+ // Invalid type suffixes.
+ "f", "d", "-f", "-d", })
+ void testIsParsableFalse(final String input) {
+ assertFalse(NumberUtils.isParsable(input));
+ }
+
+ @ParameterizedTest
+ // @formatter:off
+ @ValueSource(strings = {
+ // Decimal floating-point literals
+ "3.14f",
+ "3.14F",
+ ".5f",
+ "5.f",
+ "5f",
+ "0.0f",
+ // Exponential (scientific) notation
+ "1.23e10f",
+ "1.23E10f",
+ "1.23e-10f",
+ "1e5f",
+ ".5e3f",
+ // Hexadecimal floating-point literals
+ "0x1.8p3f",
+ "0x.8p0f",
+ "0x1p-3f",
+ "0x1.fffffep127f",
+ // With leading zeros
+ "01.5f"})
+ // @formatter:on
+ void testIsParsableFloatTrue(final String input) {
+ Float.parseFloat(input);
+ assertTrue(NumberUtils.isParsable(input));
}
/**
@@ -1056,6 +1105,39 @@ void testIsParsableFullWidthUnicodeJDK8326627() {
assertFalse(NumberUtils.isParsable("0." + fullWidth123));
}
+ @Test
+ void testIsParsableNull() {
+ // Can't use null in @ValueSource(strings)
+ assertFalse(NumberUtils.isParsable(null));
+ }
+
+ @ParameterizedTest
+ // @formatter:off
+ @ValueSource(strings = {
+ "64.",
+ "-64.",
+ "64.2",
+ "64",
+ "018",
+ ".18",
+ "-65",
+ "-018",
+ "-018.2",
+ "-.236",
+ "2.",
+ "2.f",
+ "2.d",
+ "1.2e-5f",
+ "1.2e-5d",
+ // Additional tests for scientific notation.
+ "1e5", "1E5", "1.2e5", "1.2E5", "1.2e+5", "1.2e-5", "-1.2e-5",
"1e5f", "1e5F", "1e5d", "1e5D",
+ // Additional tests for type suffixes.
+ "2f", "2F", "2d", "2D", "2.0f", "2.0F", "2.0d", "2.0D", "-2.0f",
"-2.0d" })
+ // @formatter:on
+ void testIsParsableTrue(final String input) {
+ assertTrue(NumberUtils.isParsable(input));
+ }
+
@Test
void testLang1087() {
// no sign cases
@@ -1087,8 +1169,8 @@ void testLang1729IsParsableByte() {
void testLang1729IsParsableDouble() {
assertTrue(isParsableDouble("1"));
assertTrue(isParsableDouble("1."));
- // TODO assertTrue(isParsableDouble("1.f"));
- // TODO assertTrue(isParsableDouble("1.d"));
+ assertTrue(isParsableDouble("1.f"));
+ assertTrue(isParsableDouble("1.d"));
assertTrue(isParsableDouble("1.0"));
assertFalse(isParsableDouble("1.0."));
assertFalse(isParsableDouble("1 2 3"));
@@ -1099,8 +1181,8 @@ void testLang1729IsParsableDouble() {
void testLang1729IsParsableFloat() {
assertTrue(isParsableFloat("1"));
assertTrue(isParsableFloat("1."));
- // TODO assertTrue(isParsableFloat("1.f"));
- // TODO assertTrue(isParsableFloat("1.d"));
+ assertTrue(isParsableFloat("1.f"));
+ assertTrue(isParsableFloat("1.d"));
assertTrue(isParsableFloat("1.0"));
assertFalse(isParsableFloat("1.0."));
assertFalse(isParsableFloat("1 2 3"));