mirror of
https://gitee.com/dromara/hutool.git
synced 2026-02-09 09:16:26 +08:00
add NumberFormatUtil
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Hutool Team.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cn.hutool.v7.core.math;
|
||||
|
||||
import cn.hutool.v7.core.convert.ConvertUtil;
|
||||
import cn.hutool.v7.core.text.StrUtil;
|
||||
import cn.hutool.v7.core.util.ObjUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParsePosition;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 基于NumberFormat封装的数字格式化/解析工具类
|
||||
* 提供常用的数字格式化、字符串解析为数字的方法,包含异常处理和参数校验
|
||||
*
|
||||
* @author Looly
|
||||
* @since 7.0.0
|
||||
*/
|
||||
public class NumberFormatUtil {
|
||||
|
||||
/**
|
||||
* 默认区域
|
||||
*/
|
||||
private static final Locale DEFAULT_LOCALE = Locale.getDefault(Locale.Category.FORMAT);
|
||||
|
||||
// region ----- format
|
||||
|
||||
/**
|
||||
* 使用默认区域(Locale.getDefault(Locale.Category.FORMAT))格式化数字
|
||||
*
|
||||
* @param number 要格式化的数字(支持Number子类:Integer/Long/Double/BigDecimal等)
|
||||
* @return 格式化后的字符串,null返回空字符串
|
||||
*/
|
||||
public static String format(final Number number) {
|
||||
return format(number, DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定区域格式化数字
|
||||
*
|
||||
* @param number 要格式化的数字
|
||||
* @param locale 区域(如Locale.US、Locale.CHINA)
|
||||
* @return 格式化后的字符串,null返回空字符串
|
||||
*/
|
||||
public static String format(final Number number, final Locale locale) {
|
||||
if (number == null) {
|
||||
return "";
|
||||
}
|
||||
final NumberFormat nf = NumberFormat.getInstance(locale);
|
||||
return nf.format(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字并指定小数位数
|
||||
*
|
||||
* @param number 要格式化的数字
|
||||
* @param fractionDigits 小数位数(>=0)
|
||||
* @return 格式化后的字符串
|
||||
*/
|
||||
public static String format(final Number number, final int fractionDigits) {
|
||||
return format(number, fractionDigits, Locale.getDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定区域和小数位数格式化数字
|
||||
*
|
||||
* @param number 要格式化的数字
|
||||
* @param fractionDigits 小数位数(>=0)
|
||||
* @param locale 区域
|
||||
* @return 格式化后的字符串
|
||||
*/
|
||||
public static String format(final Number number, final int fractionDigits, final Locale locale) {
|
||||
if (number == null) {
|
||||
return "";
|
||||
}
|
||||
if (fractionDigits < 0) {
|
||||
throw new IllegalArgumentException("fractionDigits must be >=0:" + fractionDigits);
|
||||
}
|
||||
final NumberFormat nf = NumberFormat.getInstance(locale);
|
||||
nf.setMaximumFractionDigits(fractionDigits);
|
||||
nf.setMinimumFractionDigits(fractionDigits);
|
||||
return nf.format(number);
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region ----- parse
|
||||
|
||||
/**
|
||||
* 使用默认区域解析字符串为数字(返回BigDecimal,避免精度丢失)
|
||||
*
|
||||
* @param source 要解析的字符串(如"1,234.56"、"1234.56")
|
||||
* @return 解析后的BigDecimal,解析失败返回null
|
||||
*/
|
||||
public static Number parse(final String source) {
|
||||
return parse(source, DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定区域解析字符串为数字
|
||||
*
|
||||
* @param source 要解析的字符串
|
||||
* @param locale 区域(解析时匹配对应格式,如Locale.US解析"1,234.56")
|
||||
* @return 解析后的BigDecimal,解析失败返回null
|
||||
*/
|
||||
public static Number parse(String source, final Locale locale) {
|
||||
// 参数校验
|
||||
source = StrUtil.trim( source);
|
||||
if(StrUtil.isEmpty(source)){
|
||||
return null;
|
||||
}
|
||||
|
||||
// issue#I79VS7 去除头部加号
|
||||
source = StrUtil.removeAllPrefix(source, "+");
|
||||
|
||||
// issue@4197@Github 转为半角
|
||||
source = ConvertUtil.toDBC(source);
|
||||
|
||||
// issue#IDJ1NS@Gitee 处理科学计数法E+格式
|
||||
// NumberFormat对E+格式支持不佳,使用BigDecimal直接解析
|
||||
if (StrUtil.containsIgnoreCase(source, "e")) {
|
||||
try {
|
||||
return new BigDecimal(source);
|
||||
} catch (final NumberFormatException e) {
|
||||
// BigDecimal解析失败,继续使用NumberFormat尝试
|
||||
}
|
||||
}
|
||||
|
||||
final NumberFormat nf = NumberFormat.getInstance(ObjUtil.defaultIfNull(locale, DEFAULT_LOCALE));
|
||||
if (nf instanceof DecimalFormat) {
|
||||
// issue#1818@Github
|
||||
// 当字符串数字超出double的长度时,会导致截断,此处使用BigDecimal接收
|
||||
((DecimalFormat) nf).setParseBigDecimal(true);
|
||||
}
|
||||
|
||||
final ParsePosition pos = new ParsePosition(0);
|
||||
final Number result = nf.parse(source, pos);
|
||||
|
||||
// 校验解析是否完全成功(pos.getIndex()等于字符串长度才是完全解析)
|
||||
if (result == null || pos.getIndex() != source.length()) {
|
||||
throw new NumberFormatException("Unparseable number: [" + source + "] at: " + pos.getIndex());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -17,11 +17,9 @@
|
||||
package cn.hutool.v7.core.math;
|
||||
|
||||
import cn.hutool.v7.core.array.ArrayUtil;
|
||||
import cn.hutool.v7.core.convert.ConvertUtil;
|
||||
import cn.hutool.v7.core.text.CharUtil;
|
||||
import cn.hutool.v7.core.text.StrUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
@@ -212,7 +210,9 @@ public class NumberParser {
|
||||
* @param numberStr 数字字符串
|
||||
* @return long
|
||||
*/
|
||||
public long parseLong(final String numberStr) {
|
||||
public long parseLong(String numberStr) {
|
||||
// 去除类型标记
|
||||
numberStr = StrUtil.removeSuffixIgnoreCase(numberStr, "L");
|
||||
if (isBlankOrNaN(numberStr)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -243,7 +243,9 @@ public class NumberParser {
|
||||
* @return long
|
||||
* @since 5.5.5
|
||||
*/
|
||||
public float parseFloat(final String numberStr) {
|
||||
public float parseFloat(String numberStr) {
|
||||
// 去除类型标记
|
||||
numberStr = StrUtil.removeSuffixIgnoreCase(numberStr, "F");
|
||||
if (isBlankOrNaN(numberStr)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -269,7 +271,9 @@ public class NumberParser {
|
||||
* @param numberStr 数字字符串
|
||||
* @return double
|
||||
*/
|
||||
public double parseDouble(final String numberStr) {
|
||||
public double parseDouble(String numberStr) {
|
||||
// 去除类型标记
|
||||
numberStr = StrUtil.removeSuffixIgnoreCase(numberStr, "D");
|
||||
if (isBlankOrNaN(numberStr)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -347,13 +351,20 @@ public class NumberParser {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 16进制
|
||||
// 16进制,需要在结尾标识解析前判断16进制,如0xFF表示16进制数,末尾的F并不能标识类型
|
||||
if (StrUtil.startWithIgnoreCase(numberStr, "0x")) {
|
||||
// 0x04表示16进制数
|
||||
return Long.parseLong(numberStr.substring(2), 16);
|
||||
return parseLong(numberStr);
|
||||
}
|
||||
|
||||
return doParse(numberStr);
|
||||
// 以特殊字母结尾的数字,按照其对应类型解析返回
|
||||
final char lastChar = Character.toUpperCase(numberStr.charAt(numberStr.length() - 1));
|
||||
return switch (lastChar) {
|
||||
case 'F' -> parseFloat(numberStr);
|
||||
case 'D' -> parseDouble(numberStr);
|
||||
case 'L' -> parseLong(numberStr);
|
||||
default -> doParse(numberStr);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,42 +373,8 @@ public class NumberParser {
|
||||
*
|
||||
* @return 数字
|
||||
*/
|
||||
private Number doParse(String numberStr) {
|
||||
Locale locale = this.locale;
|
||||
if (null == locale) {
|
||||
locale = Locale.getDefault(Locale.Category.FORMAT);
|
||||
}
|
||||
if (StrUtil.startWith(numberStr, CharUtil.PLUS)) {
|
||||
// issue#I79VS7
|
||||
numberStr = StrUtil.subSuf(numberStr, 1);
|
||||
}
|
||||
|
||||
// issue@4197@Github 转为半角
|
||||
numberStr = ConvertUtil.toDBC(numberStr);
|
||||
|
||||
// issue#IDJ1NS@Gitee 处理科学计数法E+格式
|
||||
// NumberFormat对E+格式支持不佳,使用BigDecimal直接解析
|
||||
if (StrUtil.containsIgnoreCase(numberStr, "e")) {
|
||||
try {
|
||||
return new BigDecimal(numberStr);
|
||||
} catch (final NumberFormatException e) {
|
||||
// BigDecimal解析失败,继续使用NumberFormat尝试
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
private Number doParse(final String numberStr) {
|
||||
return NumberFormatUtil.parse(numberStr, this.locale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
package cn.hutool.v7.core.convert;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* https://github.com/chinabugotech/hutool/issues/3105
|
||||
*/
|
||||
@@ -26,6 +27,6 @@ public class Issue3105Test {
|
||||
@Test
|
||||
void toLongTest() {
|
||||
final Long aLong = ConvertUtil.toLong("0.a");
|
||||
Assertions.assertEquals(0L, aLong);
|
||||
assertNull(aLong);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@ import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class IssueIALV38Test {
|
||||
|
||||
/**
|
||||
* V7版本变更,旧版本不会抛出异常,新版本中无法解析非正常数字
|
||||
*/
|
||||
@Test
|
||||
void name() {
|
||||
final Object o = ConvertUtil.convertWithCheck(BigDecimal.class, " 111啊", null, false);
|
||||
Assertions.assertEquals(new BigDecimal("111"), o);
|
||||
void convertWithCheckTest() {
|
||||
Assertions.assertThrows(NumberFormatException.class, ()-> ConvertUtil.convertWithCheck(BigDecimal.class, " 111啊", null, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package cn.hutool.v7.core.math;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -25,8 +24,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
public class NumberParserTest {
|
||||
@Test
|
||||
void parseLongTest() {
|
||||
final long value = NumberParser.INSTANCE.parseLong("0.a");
|
||||
Assertions.assertEquals(0L, value);
|
||||
// 0.a是非法数字,抛出NumberFormatException
|
||||
assertThrows(NumberFormatException.class, () -> NumberParser.INSTANCE.parseLong("0.a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -396,9 +396,8 @@ public class NumberUtilTest {
|
||||
@Test
|
||||
public void issue2878Test() throws ParseException {
|
||||
// https://github.com/chinabugotech/hutool/issues/2878
|
||||
// 当数字中包含一些非数字字符时,按照JDK的规则,不做修改。
|
||||
final BigDecimal bigDecimal = NumberUtil.toBigDecimal("345.sdf");
|
||||
assertEquals(NumberFormat.getInstance().parse("345.sdf"), bigDecimal.longValue());
|
||||
// 当数字中包含一些非数字字符时,V7版本中抛出异常
|
||||
assertThrows(NumberFormatException.class, () -> NumberUtil.toBigDecimal("345.sdf"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -419,20 +418,18 @@ public class NumberUtilTest {
|
||||
number = NumberUtil.parseInt(" ");
|
||||
assertEquals(0, number);
|
||||
|
||||
number = NumberUtil.parseInt("10F");
|
||||
assertEquals(10, number);
|
||||
|
||||
number = NumberUtil.parseInt("22.4D");
|
||||
assertEquals(22, number);
|
||||
|
||||
number = NumberUtil.parseInt("22.6D");
|
||||
assertEquals(22, number);
|
||||
// 浮点数解析为int时会造成精度丢失
|
||||
assertThrows(NumberFormatException.class, () -> {
|
||||
NumberUtil.parseInt("22.4D");
|
||||
});
|
||||
|
||||
number = NumberUtil.parseInt("0");
|
||||
assertEquals(0, number);
|
||||
|
||||
number = NumberUtil.parseInt(".123");
|
||||
assertEquals(0, number);
|
||||
number = NumberUtil.parseInt(".999");
|
||||
assertEquals(0, number);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -471,9 +468,7 @@ public class NumberUtilTest {
|
||||
assertEquals(456, NumberUtil.parseInt("abc", 456));
|
||||
|
||||
// -------------------------- Parse success -----------------------
|
||||
|
||||
assertEquals(123, NumberUtil.parseInt("123.abc", 789));
|
||||
|
||||
assertEquals(789, NumberUtil.parseInt("123.abc", 789));
|
||||
assertEquals(123, NumberUtil.parseInt("123.3", null));
|
||||
|
||||
}
|
||||
@@ -520,11 +515,11 @@ public class NumberUtilTest {
|
||||
|
||||
// -------------------------- Parse success -----------------------
|
||||
|
||||
assertEquals(123, NumberUtil.parseNumber("123.abc", 789).intValue());
|
||||
assertEquals(789, NumberUtil.parseNumber("123.abc", 789).intValue());
|
||||
|
||||
assertEquals(123.3D, NumberUtil.parseNumber("123.3", (Number) null).doubleValue());
|
||||
|
||||
assertEquals(0.123D, NumberUtil.parseNumber("0.123.3", (Number) null).doubleValue());
|
||||
assertNull(NumberUtil.parseNumber("0.123.3", (Number) null));
|
||||
|
||||
}
|
||||
|
||||
@@ -567,14 +562,15 @@ public class NumberUtilTest {
|
||||
number = NumberUtil.parseLong(" ");
|
||||
assertEquals(0, number);
|
||||
|
||||
number = NumberUtil.parseLong("10F");
|
||||
assertEquals(10, number);
|
||||
// 明确float类型时,解析long报错,避免精度丢失问题
|
||||
assertThrows(NumberFormatException.class, () -> {
|
||||
NumberUtil.parseLong("10F");
|
||||
});
|
||||
|
||||
number = NumberUtil.parseLong("22.4D");
|
||||
assertEquals(22, number);
|
||||
|
||||
number = NumberUtil.parseLong("22.6D");
|
||||
assertEquals(22, number);
|
||||
// 明确double类型时,解析long报错,避免精度丢失问题
|
||||
assertThrows(NumberFormatException.class, () -> {
|
||||
NumberUtil.parseLong("22.4D");
|
||||
});
|
||||
|
||||
number = NumberUtil.parseLong("0");
|
||||
assertEquals(0, number);
|
||||
@@ -718,6 +714,7 @@ public class NumberUtilTest {
|
||||
assertFalse(NumberUtil.isPrime(296733));
|
||||
assertFalse(NumberUtil.isPrime(20_4123_2399));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isPrimeTest2() {
|
||||
assertTrue(NumberUtil.isPrime(2));
|
||||
@@ -743,14 +740,15 @@ public class NumberUtilTest {
|
||||
assertNull(NumberUtil.parseFloat("abc", null));
|
||||
|
||||
assertNull(NumberUtil.parseFloat("a123.33", null));
|
||||
assertNull(NumberUtil.parseFloat("123.33a", null));
|
||||
|
||||
assertNull(NumberUtil.parseFloat("..123", null));
|
||||
|
||||
|
||||
assertEquals(1233F, NumberUtil.parseFloat(StrUtil.EMPTY, 1233F));
|
||||
|
||||
// -------------------------- Parse success -----------------------
|
||||
|
||||
assertEquals(123.33F, NumberUtil.parseFloat("123.33a", null));
|
||||
|
||||
assertEquals(0.123F, NumberUtil.parseFloat(".123", null));
|
||||
|
||||
@@ -766,9 +764,9 @@ public class NumberUtilTest {
|
||||
assertNull(NumberUtil.parseDouble("..123", null));
|
||||
assertEquals(1233D, NumberUtil.parseDouble(StrUtil.EMPTY, 1233D));
|
||||
|
||||
assertThrows(NumberFormatException.class, () -> NumberUtil.parseDouble("123.33a"));
|
||||
// -------------------------- Parse success -----------------------
|
||||
|
||||
assertEquals(123.33D, NumberUtil.parseDouble("123.33a", null));
|
||||
assertEquals(0.123D, NumberUtil.parseDouble(".123", null));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user