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

Reply via email to