diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/func/LambdaFactory.java b/hutool-core/src/main/java/org/dromara/hutool/core/func/LambdaFactory.java index 139016b1e..40c94b76d 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/func/LambdaFactory.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/func/LambdaFactory.java @@ -26,7 +26,7 @@ import java.util.Map; /** * 以类似反射的方式动态创建Lambda,在性能上有一定优势,同时避免每次调用Lambda时创建匿名内部类 - * TODO JDK9+存在兼容问题 + * TODO JDK9+存在兼容问题,当参数为原始类型时报错 * * @author nasodaengineer */ @@ -122,7 +122,6 @@ public class LambdaFactory { final String invokeName = invokeMethod.getName(); final MethodType invokedType = MethodType.methodType(funcType); - // 对入参做检查,原始类型转换为包装类型 final Class[] paramTypes = invokeMethod.getParameterTypes(); final MethodType samMethodType = MethodType.methodType(invokeMethod.getReturnType(), paramTypes); diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/math/NumberParser.java b/hutool-core/src/main/java/org/dromara/hutool/core/math/NumberParser.java new file mode 100644 index 000000000..0c5af9d8e --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/math/NumberParser.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.math; + +import org.dromara.hutool.core.text.StrUtil; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; + +/** + * 数字解析器
+ * 用于将字符串解析为对应的数字类型,支持包括: + * + * + *

+ * 构造时可选是否将NaN转为0,默认为true。
+ * 参考:https://stackoverflow.com/questions/5876369/why-does-casting-double-nan-to-int-not-throw-an-exception-in-java + *

+ * + * @author looly + * @since 6.0.0 + */ +public class NumberParser { + + /** + * 单例 + */ + public static final NumberParser INSTANCE = of(null); + + /** + * 构建NumberParser + * + * @param locale 地域,{@code null}表示默认本地 + * @return NumberParser + */ + public static NumberParser of(final Locale locale) { + return of(locale, true); + } + + /** + * 构建NumberParser + * + * @param locale 地域,{@code null}表示默认本地 + * @param zeroIfNaN 如果用户传入NaN,是否转为0,如果为{@code false},则抛出{@link NumberFormatException} + * @return NumberParser + */ + public static NumberParser of(final Locale locale, final boolean zeroIfNaN) { + return new NumberParser(locale, zeroIfNaN); + } + + private static final String NaN = "NaN"; + + private final Locale locale; + private final boolean zeroIfNaN; + + /** + * 构造 + * + * @param locale 地域,{@code null}表示默认本地 + * @param zeroIfNaN 如果用户传入NaN,是否转为0,如果为{@code false},则抛出{@link NumberFormatException} + */ + public NumberParser(final Locale locale, final boolean zeroIfNaN) { + this.locale = locale; + this.zeroIfNaN = zeroIfNaN; + } + + /** + * 解析转换数字字符串为int型数字,规则如下: + * + *
+	 * 1、0x开头的视为16进制数字
+	 * 2、0开头的忽略开头的0
+	 * 3、其它情况按照10进制转换
+	 * 4、空串返回0
+	 * 5、.123形式返回0(按照小于0的小数对待)
+	 * 6、123.56截取小数点之前的数字,忽略小数部分
+	 * 7、科学计数法抛出NumberFormatException异常
+	 * 
+ * + * @param numberStr 数字字符串 + * @return int + * @throws NumberFormatException 数字格式异常 + */ + public int parseInt(final String numberStr) throws NumberFormatException { + if (isBlankOrNaN(numberStr)) { + return 0; + } + + if (StrUtil.containsIgnoreCase(numberStr, "E")) { + // 科学计数法忽略支持,科学计数法一般用于表示非常小和非常大的数字,这类数字转换为int后精度丢失,没有意义。 + throw new NumberFormatException(StrUtil.format("Unsupported int format: [{}]", numberStr)); + } + + if (StrUtil.startWithIgnoreCase(numberStr, "0x")) { + // 0x04表示16进制数 + return Integer.parseInt(numberStr.substring(2), 16); + } + + try { + return Integer.parseInt(numberStr); + } catch (final NumberFormatException e) { + return doParse(numberStr).intValue(); + } + } + + /** + * 解析转换数字字符串为long型数字,规则如下: + * + *
+	 * 1、0x开头的视为16进制数字
+	 * 2、0开头的忽略开头的0
+	 * 3、空串返回0
+	 * 4、其它情况按照10进制转换
+	 * 5、.123形式返回0(按照小于0的小数对待)
+	 * 6、123.56截取小数点之前的数字,忽略小数部分
+	 * 
+ * + * @param numberStr 数字字符串 + * @return long + */ + public long parseLong(final String numberStr) { + if (isBlankOrNaN(numberStr)) { + return 0; + } + + if (StrUtil.startWithIgnoreCase(numberStr, "0x")) { + // 0x04表示16进制数 + return Long.parseLong(numberStr.substring(2), 16); + } + + try { + return Long.parseLong(numberStr); + } catch (final NumberFormatException e) { + return doParse(numberStr).longValue(); + } + } + + /** + * 解析转换数字字符串为long型数字,规则如下: + * + *
+	 * 1、0开头的忽略开头的0
+	 * 2、空串返回0
+	 * 3、其它情况按照10进制转换
+	 * 4、.123形式返回0.123(按照小于0的小数对待)
+	 * 
+ * + * @param numberStr 数字字符串 + * @return long + * @since 5.5.5 + */ + public float parseFloat(final String numberStr) { + if (isBlankOrNaN(numberStr)) { + return 0; + } + + try { + return Float.parseFloat(numberStr); + } catch (final NumberFormatException e) { + return doParse(numberStr).floatValue(); + } + } + + /** + * 解析转换数字字符串为long型数字,规则如下: + * + *
+	 * 1、0开头的忽略开头的0
+	 * 2、空串返回0
+	 * 3、其它情况按照10进制转换
+	 * 4、.123形式返回0.123(按照小于0的小数对待)
+	 * 5、NaN返回0
+	 * 
+ * + * @param numberStr 数字字符串 + * @return double + */ + public double parseDouble(final String numberStr) { + if (isBlankOrNaN(numberStr)) { + return 0; + } + + try { + return Double.parseDouble(numberStr); + } catch (final NumberFormatException e) { + return doParse(numberStr).doubleValue(); + } + } + + /** + * 将指定字符串转换为{@link Number} 对象
+ * 此方法不支持科学计数法 + * + * + * + *

+ * 需要注意的是,在不同Locale下,数字的表示形式也是不同的,例如:
+ * 德国、荷兰、比利时、丹麦、意大利、罗马尼亚和欧洲大多地区使用`,`区分小数
+ * 也就是说,在这些国家地区,1.20表示120,而非1.2。 + *

+ * + * @param numberStr 数字字符串 + * @return Number对象 + * @throws NumberFormatException 包装了{@link ParseException},当给定的数字字符串无法解析时抛出 + */ + public Number parseNumber(final String numberStr) throws NumberFormatException { + if (isBlankOrNaN(numberStr)) { + // 在JDK9+中,NaN的处理逻辑是返回0,此处保持一致 + return 0; + } + + // 16进制 + if (StrUtil.startWithIgnoreCase(numberStr, "0x")) { + // 0x04表示16进制数 + return Long.parseLong(numberStr.substring(2), 16); + } + + return doParse(numberStr); + } + + /** + * 使用{@link NumberFormat} 完成数字解析
+ * 如果为{@link DecimalFormat} + * + * @return 数字 + */ + private Number doParse(final String numberStr) { + Locale locale = this.locale; + if (null == locale) { + locale = Locale.getDefault(Locale.Category.FORMAT); + } + try { + final NumberFormat format = NumberFormat.getInstance(locale); + if (format instanceof DecimalFormat) { + // issue#1818@Github + // 当字符串数字超出double的长度时,会导致截断,此处使用BigDecimal接收 + ((DecimalFormat) format).setParseBigDecimal(true); + } + return format.parse(numberStr); + } catch (final ParseException e) { + final NumberFormatException nfe = new NumberFormatException(e.getMessage()); + nfe.initCause(e); + throw nfe; + } + } + + /** + * 是否空白串或者NaN
+ * 如果{@link #zeroIfNaN}为{@code false},则抛出{@link NumberFormatException} + * + * @return 是否空白串或者NaN + */ + private boolean isBlankOrNaN(final String numberStr) throws NumberFormatException { + if (StrUtil.isBlank(numberStr)) { + return true; + } + + if (NaN.equals(numberStr)) { + if (zeroIfNaN) { + return true; + } else { + throw new NumberFormatException("Can not parse NaN when 'zeroIfNaN' is false!"); + } + } + + return false; + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/math/NumberUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/math/NumberUtil.java index 510f31461..45fdab884 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/math/NumberUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/math/NumberUtil.java @@ -1425,31 +1425,13 @@ public class NumberUtil { * 7、科学计数法抛出NumberFormatException异常 * * - * @param number 数字,支持0x开头、0开头和普通十进制 + * @param numberStr 数字,支持0x开头、0开头和普通十进制 * @return int * @throws NumberFormatException 数字格式异常 * @since 4.1.4 */ - public static int parseInt(final String number) throws NumberFormatException { - if (StrUtil.isBlank(number)) { - return 0; - } - - if (StrUtil.containsIgnoreCase(number, "E")) { - // 科学计数法忽略支持,科学计数法一般用于表示非常小和非常大的数字,这类数字转换为int后精度丢失,没有意义。 - throw new NumberFormatException(StrUtil.format("Unsupported int format: [{}]", number)); - } - - if (StrUtil.startWithIgnoreCase(number, "0x")) { - // 0x04表示16进制数 - return Integer.parseInt(number.substring(2), 16); - } - - try { - return Integer.parseInt(number); - } catch (final NumberFormatException e) { - return parseNumber(number).intValue(); - } + public static int parseInt(final String numberStr) throws NumberFormatException { + return NumberParser.INSTANCE.parseInt(numberStr); } /** @@ -1493,25 +1475,12 @@ public class NumberUtil { * 6、123.56截取小数点之前的数字,忽略小数部分 * * - * @param number 数字,支持0x开头、0开头和普通十进制 + * @param numberStr 数字,支持0x开头、0开头和普通十进制 * @return long * @since 4.1.4 */ - public static long parseLong(final String number) { - if (StrUtil.isBlank(number)) { - return 0L; - } - - if (StrUtil.startWithIgnoreCase(number, "0x")) { - // 0x04表示16进制数 - return Long.parseLong(number.substring(2), 16); - } - - try { - return Long.parseLong(number); - } catch (final NumberFormatException e) { - return parseNumber(number).longValue(); - } + public static long parseLong(final String numberStr) { + return NumberParser.INSTANCE.parseLong(numberStr); } /** @@ -1550,20 +1519,12 @@ public class NumberUtil { * 4、.123形式返回0.123(按照小于0的小数对待) * * - * @param number 数字,支持0x开头、0开头和普通十进制 + * @param numberStr 数字,支持0x开头、0开头和普通十进制 * @return long * @since 5.5.5 */ - public static float parseFloat(final String number) { - if (StrUtil.isBlank(number)) { - return 0f; - } - - try { - return Float.parseFloat(number); - } catch (final NumberFormatException e) { - return parseNumber(number).floatValue(); - } + public static float parseFloat(final String numberStr) { + return NumberParser.INSTANCE.parseFloat(numberStr); } /** @@ -1599,22 +1560,15 @@ public class NumberUtil { * 2、空串返回0 * 3、其它情况按照10进制转换 * 4、.123形式返回0.123(按照小于0的小数对待) + * 5、NaN返回0 * * - * @param number 数字,支持0x开头、0开头和普通十进制 - * @return long + * @param numberStr 数字,支持0x开头、0开头和普通十进制 + * @return double * @since 5.5.5 */ - public static double parseDouble(final String number) { - if (StrUtil.isBlank(number)) { - return 0D; - } - - try { - return Double.parseDouble(number); - } catch (final NumberFormatException e) { - return parseNumber(number).doubleValue(); - } + public static double parseDouble(final String numberStr) { + return NumberParser.INSTANCE.parseDouble(numberStr); } /** @@ -1652,7 +1606,7 @@ public class NumberUtil { * @since 4.1.15 */ public static Number parseNumber(final String numberStr) throws NumberFormatException { - return parseNumber(numberStr, Locale.getDefault(Locale.Category.FORMAT)); + return NumberParser.INSTANCE.parseNumber(numberStr); } /** @@ -1671,24 +1625,7 @@ public class NumberUtil { * @throws NumberFormatException 包装了{@link ParseException},当给定的数字字符串无法解析时抛出 */ public static Number parseNumber(final String numberStr, final Locale locale) throws NumberFormatException { - if (StrUtil.startWithIgnoreCase(numberStr, "0x")) { - // 0x04表示16进制数 - return Long.parseLong(numberStr.substring(2), 16); - } - - try { - final NumberFormat format = NumberFormat.getInstance(locale); - if (format instanceof DecimalFormat) { - // issue#1818@Github - // 当字符串数字超出double的长度时,会导致截断,此处使用BigDecimal接收 - ((DecimalFormat) format).setParseBigDecimal(true); - } - return format.parse(numberStr); - } catch (final ParseException e) { - final NumberFormatException nfe = new NumberFormatException(e.getMessage()); - nfe.initCause(e); - throw nfe; - } + return NumberParser.of(locale).parseNumber(numberStr); } // endregion diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/ObjUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/util/ObjUtil.java index df98ed89d..d8cb4030a 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/util/ObjUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/util/ObjUtil.java @@ -56,7 +56,7 @@ public class ObjUtil { public static boolean equals(final Object obj1, final Object obj2) { if (obj1 instanceof Number && obj2 instanceof Number) { return NumberUtil.equals((Number) obj1, (Number) obj2); - } else if(ArrayUtil.isArray(obj1) && ArrayUtil.isArray(obj2)){ + } else if (ArrayUtil.isArray(obj1) && ArrayUtil.isArray(obj2)) { return ArrayUtil.equals(obj1, obj2); } return Objects.equals(obj1, obj2); @@ -68,8 +68,8 @@ public class ObjUtil { * @param obj1 对象1 * @param obj2 对象2 * @return 是否不等 - * @since 3.0.7 * @see #equals(Object, Object) + * @since 3.0.7 */ public static boolean notEquals(final Object obj1, final Object obj2) { return !equals(obj1, obj2); @@ -229,12 +229,12 @@ public class ObjUtil { * * @param obj 被判断的对象 * @return 是否为空,如果类型不支持,返回false - * @since 4.5.7 * @see StrUtil#isEmpty(CharSequence) * @see MapUtil#isEmpty(Map) * @see IterUtil#isEmpty(Iterable) * @see IterUtil#isEmpty(Iterator) * @see ArrayUtil#isEmpty(Object) + * @since 4.5.7 */ @SuppressWarnings("rawtypes") public static boolean isEmpty(final Object obj) { @@ -244,9 +244,9 @@ public class ObjUtil { if (obj instanceof CharSequence) { return StrUtil.isEmpty((CharSequence) obj); - } else if(obj instanceof Collection){ - return CollUtil.isEmpty((Collection)obj); - }else if (obj instanceof Map) { + } else if (obj instanceof Collection) { + return CollUtil.isEmpty((Collection) obj); + } else if (obj instanceof Map) { return MapUtil.isEmpty((Map) obj); } else if (obj instanceof Iterable) { return IterUtil.isEmpty((Iterable) obj); @@ -264,13 +264,15 @@ public class ObjUtil { * * @param obj 被判断的对象 * @return 是否不为空,如果类型不支持,返回true - * @since 4.5.7 * @see #isEmpty(Object) + * @since 4.5.7 */ public static boolean isNotEmpty(final Object obj) { return !isEmpty(obj); } + // region ----- defaultIf + /** *

如果给定对象为{@code null}返回默认值 *

{@code
@@ -340,6 +342,7 @@ public class ObjUtil {
 		final T source, final Function handler, final R defaultValue) {
 		return isNull(source) ? defaultValue : handler.apply(source);
 	}
+	// endregion
 
 	/**
 	 * 

克隆对象 @@ -451,8 +454,8 @@ public class ObjUtil { * @param obj 被检查的实体对象 * @param index 泛型类型的索引号,即第几个泛型类型 * @return {@link Class} - * @since 3.0.8 * @see ClassUtil#getTypeArgument(Class, int) + * @since 3.0.8 */ public static Class getTypeArgument(final Object obj, final int index) { return ClassUtil.getTypeArgument(obj.getClass(), index); @@ -468,8 +471,8 @@ public class ObjUtil { * * @param obj Bean对象 * @return 转换后的字符串 - * @since 3.2.0 * @see Convert#toStr(Object) + * @since 3.2.0 */ public static String toString(final Object obj) { if (null == obj) { diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/NumberUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/util/NumberUtilTest.java index b390970c6..1b6dbb4c5 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/util/NumberUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/util/NumberUtilTest.java @@ -5,6 +5,8 @@ import org.dromara.hutool.core.math.NumberUtil; import org.dromara.hutool.core.text.StrUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import java.math.BigDecimal; import java.math.RoundingMode; @@ -698,18 +700,26 @@ public class NumberUtilTest { // -------------------------- Parse failed ----------------------- Assertions.assertNull(NumberUtil.parseDouble("abc", null)); - Assertions.assertNull(NumberUtil.parseDouble("a123.33", null)); - Assertions.assertNull(NumberUtil.parseDouble("..123", null)); - Assertions.assertEquals(1233D, NumberUtil.parseDouble(StrUtil.EMPTY, 1233D)); // -------------------------- Parse success ----------------------- Assertions.assertEquals(123.33D, NumberUtil.parseDouble("123.33a", null)); - Assertions.assertEquals(0.123D, NumberUtil.parseDouble(".123", null)); + } + @Test + void naNToIntTest() { + Assertions.assertEquals(0, Double.valueOf(Double.NaN).intValue()); + } + + @Test + @EnabledForJreRange(max = JRE.JAVA_8) + void numberFormatTest() { + Assertions.assertThrows(ParseException.class, ()->{ + NumberFormat.getInstance().parse("NaN"); + }); } } diff --git a/pom.xml b/pom.xml index b0b081811..d3d9e40e4 100755 --- a/pom.xml +++ b/pom.xml @@ -139,9 +139,6 @@ org.apache.maven.plugins maven-surefire-plugin 3.0.0 - - --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED - org.apache.maven.plugins