add NumberFormatUtil

This commit is contained in:
Looly
2026-01-22 00:57:48 +08:00
parent 5aa24a113c
commit 67d579a7e6
6 changed files with 247 additions and 107 deletions

View File

@@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

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