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 2ef56f4d952..6abd66502a5 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 boolean accept(final Consumer 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}. *

* - * @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) { *

* *

- * Hexadecimal and scientific notations are not 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)}. *

* *

@@ -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. - * - *

- * - * @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 528039c9cf6..105dda46204 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"));