This commit is contained in:
Looly 2023-04-22 20:21:49 +08:00
parent dfa5dab392
commit 2d9febc7fb
6 changed files with 332 additions and 97 deletions

View File

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

View File

@ -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;
/**
* 数字解析器<br>
* 用于将字符串解析为对应的数字类型支持包括
* <ul>
* <li>0开头的忽略开头的0</li>
* <li>空串返回0</li>
* <li>NaN返回0</li>
* <li>其它情况按照10进制转换</li>
* <li>.123形式返回0.123按照小于0的小数对待</li>
* </ul>
*
* <p>
* 构造时可选是否将NaN转为0默认为true<br>
* 参考https://stackoverflow.com/questions/5876369/why-does-casting-double-nan-to-int-not-throw-an-exception-in-java
* </p>
*
* @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型数字规则如下
*
* <pre>
* 10x开头的视为16进制数字
* 20开头的忽略开头的0
* 3其它情况按照10进制转换
* 4空串返回0
* 5.123形式返回0按照小于0的小数对待
* 6123.56截取小数点之前的数字忽略小数部分
* 7科学计数法抛出NumberFormatException异常
* </pre>
*
* @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型数字规则如下
*
* <pre>
* 10x开头的视为16进制数字
* 20开头的忽略开头的0
* 3空串返回0
* 4其它情况按照10进制转换
* 5.123形式返回0按照小于0的小数对待
* 6123.56截取小数点之前的数字忽略小数部分
* </pre>
*
* @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型数字规则如下
*
* <pre>
* 10开头的忽略开头的0
* 2空串返回0
* 3其它情况按照10进制转换
* 4.123形式返回0.123按照小于0的小数对待
* </pre>
*
* @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型数字规则如下
*
* <pre>
* 10开头的忽略开头的0
* 2空串返回0
* 3其它情况按照10进制转换
* 4.123形式返回0.123按照小于0的小数对待
* 5NaN返回0
* </pre>
*
* @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} 对象<br>
* 此方法不支持科学计数法
*
* <ul>
* <li>空白符和NaN转换为0</li>
* <li>0x开头使用16进制解析为Long类型</li>
* </ul>
*
* <p>
* 需要注意的是在不同Locale下数字的表示形式也是不同的例如<br>
* 德国荷兰比利时丹麦意大利罗马尼亚和欧洲大多地区使用`,`区分小数<br>
* 也就是说在这些国家地区1.20表示120而非1.2
* </p>
*
* @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} 完成数字解析<br>
* 如果为{@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<br>
* 如果{@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;
}
}

View File

@ -1425,31 +1425,13 @@ public class NumberUtil {
* 7科学计数法抛出NumberFormatException异常
* </pre>
*
* @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 {
* 6123.56截取小数点之前的数字忽略小数部分
* </pre>
*
* @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的小数对待
* </pre>
*
* @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的小数对待
* 5NaN返回0
* </pre>
*
* @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

View File

@ -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
/**
* <p>如果给定对象为{@code null}返回默认值
* <pre>{@code
@ -340,6 +342,7 @@ public class ObjUtil {
final T source, final Function<? super T, ? extends R> handler, final R defaultValue) {
return isNull(source) ? defaultValue : handler.apply(source);
}
// endregion
/**
* <p>克隆对象
@ -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) {

View File

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

View File

@ -139,9 +139,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>