Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 18 additions & 48 deletions src/main/java/org/apache/commons/lang3/math/NumberUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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>
Expand All @@ -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) {
Expand Down
150 changes: 116 additions & 34 deletions src/test/java/org/apache/commons/lang3/math/NumberUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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));
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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"));
Expand All @@ -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"));
Expand Down