最近做需求,需要用到数字人民币转大写的方法,项目里面本身就有相应的工具方法,我就直接拿来用,结果到了生产上就有bug,原来是double类型十进制转二进制后小数点后的精度问题。于是自己总结写了一个比较健全的,顺便把对应的坑贴一下,提醒自己
先上正确的代码,需要的同学可以直接拿走,想要了解注意事项的同学往代码下面翻
public static String digitCapital(double n) {
String fraction[] = {"角", "分"};
String digit[] = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
String unit[][] = {{"元", "万", "亿"}, {"", "拾", "佰", "仟"}};
String head = n < 0 ? "负" : "";
// 如果是负数取绝对值
n = Math.abs(n);
String s = "";
BigDecimal bigDecimal = null;
String value = String.valueOf(n);
if(value.contains("E")){
bigDecimal = new BigDecimal(n);
}else{
bigDecimal = new BigDecimal(value);
}
String nStr = bigDecimal.toString();
// 小数部分
String[] split = nStr.split("\\.");
if (split.length > 1) {
// 小数点为特殊符号,在分割时需进行转义
String decimalStr = split[1];
if (decimalStr.length() > 2) {
decimalStr = decimalStr.substring(0, 2);
}
// 将小数部分转换为整数
Integer integer = Integer.valueOf(decimalStr);
String p = "";
for (int i = 0; i < decimalStr.length() && i < fraction.length; i++) {
p = digit[integer % 10] + fraction[decimalStr.length() - i - 1] + p;
integer = integer / 10;
}
s = p.replaceAll("(零.)+", "") + s;
}
if (s.length() < 1) {
s = "整";
}
int integerPart = (int)Math.floor(n);
// 整数部分
for (int i = 0; i < unit[0].length && integerPart > 0; i++) {
String p = "";
for (int j = 0; j < unit[1].length && n > 0; j++) {
p = digit[integerPart % 10] + unit[1][j] + p;
integerPart = integerPart / 10;
}
s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s;
}
return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整");
}
接下来说一下注意事项:
1、网上大多数是直接使用double类型,没有转bigDecimal,这样就把double类型十进制转二进制精度丢失的天然属性问题带进来了,比如会把 “82602.65” 转成 “捌万贰仟陆佰零贰元陆角肆分” ,底层机制分析一下。
先按常规的十进制转二进制逻辑来一遍(稍后再反着转一遍看得更明白)
(1) 十进制整数如何转化为二进制数
算法很简单。82602表示成二进制数:
82602/2=41301 余 0
41301/2=20650 余 1
20650/2=10325 余 0
10325/2=5162 余 1
5162/2=2581 余 0
2581/2=1290 余 1
1290/2=645 余 0
645/2=322 余 1
322/2=161 余 0
161/2=80 余 1
80/2=40 余 0
40/2=20 余 0
20/2=10 余 0
10/2=5 余 0
5/2=2 余 1
2/2=1 余 0
1/2=0 余 1
0结束 82602二进制表示为(从下往上):10100001010101010
这里提一点:只要遇到除以后的结果为0了就结束了,大家想一想,
所有的整数除以2是不是一定能够最终得到0。
换句话说,所有的整数转变为二进制数的算法会不会无限循环下去呢?
绝对不会,整数永远可以用二进制精确表示 ,但小数就不一定了。
(2) 十进制小数如何转化为二进制数
算法是乘以2直到没有了小数为止。0.65表示成二进制数
0.65*2=1.3 取整数部分 1
0.3(1.3的小数部分)*2=0.6 取整数部分 0
0.6*2=1.2 取整数部分 1
0.2*2=0.4 取整数部分 0
0.4*2=0.8 取整数部分 0
0.8*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 1(这个时候开始了循环)
......... 0.65二进制表示为(从上往下): 101001100110011......
注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,
这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。
其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统
也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的
精度丢失问题。
我们得把10100001010101010.101001100110011.…放进一个 double 双精度浮点数里面
双精度浮点数能表示多少精度呢?查看文档会发现:
半精度(16bit):11位有效数字
单精度(32bit):24位有效数字
双精度(64bit):53位有效数字
四精度(128bit):113位有效数字
好吧,双精度是53位有效数字,那么上面十进制的82602.65在计算机中的
二进制就是10100001010101010.10100110011001100110011001100110011
然后我们再把二进制转换回十进制,整数部分不赘述还是82602;
小数部分是1*2^-1+0*2^-2+1*2^-3+0*2^-4+0*2^-5+1*2^-6+1*2^-7
+0*2^-8+0*2^-9+1*2^-10+1*2^-11
+0*2^-12+0*2^-13+1*2^-14+1*2^-15
+0*2^-16+0*2^-17+1*2^-18+1*2^-19
+0*2^-20+0*2^-21+1*2^-22+1*2^-23
+0*2^-24+0*2^-25+1*2^-26+1*2^-27
+0*2^-28+0*2^-29+1*2^-30+1*2^-31
+0*2^-32+0*2^-33+1*2^-34+1*2^-35
得到了十进制小数0.6499999999941792,这好好的82602.65就变成了
82602.6499999999941792,转换成中文大写会自动丢弃分之后的数据,
所以就少了一分钱。
2、double类型在大数据时会采用科学计数法表示数值。
比如150000000就会使用1.5E8来表示,如果这里直接使用Double.toString()方法,它真的就会变成字符串“1.5E8”,后面就会直接报错了