利用润乾自带显示格式所带来的四舍六入五成双问题
第55章 利用润乾自带显示格式所带来的四舍六入五成双问题
1. 问题概述
在润乾V4版本中使用显示格式:#,对单元格中的值进行四舍五入时会有一个问题:单元格中写了一个小数,如4.5,设置其显示格式为:#,预览的结果是4;若将4.5改成4.6或者4.51,设置显示格式为#,预览结果为5。遇到这种情况我们通常都会想到可以利用润乾自带的函数round()进行四舍五入的处理,
但如果客户的数据是利用自动计算里得到的,那就不能使用round() 来完成了,而用显示格式#,就会带来这样的问题,咨询研发得知,报表在显示格式化的时候利用的是:
NumberFormat numberFormat = new DecimalFormat(“#0.##”); 的java规则:
四舍六入,奇数入,偶数不入。
即数据”3.5″设置其显示格式为#,则得到的结果是4
数据”4.5″设置其显示格式为#,则得到的结果也是4
其实很多语言都使用的”四舍六入”的规则。 从统计学的角度,”四舍六入五成双”比”四舍五入”要科学,在大量运算时,它使舍入后的结果误差的均值趋于零,而不是像四舍五入那样逢五就入,导致结果偏向大数,使得误差产生积累进而产生系统误差,”四舍六入五成双”使测量结果受到舍入误差的影响降到最低。
其实这并不是我们的BUG,所有java的各个版本JDK均使用此规则。四舍五入还是”六入五成双”,不是简单的哪个更合理的问题。因为客户有它的客户,每个客户有自己内部一套规则。而且四舍五入恐怕对绝大多数人都是习以为常的。
2. 解决方案
功能改进贡献者:郑谷川,周辉辉,周俊,蔡博文,此次向大家介绍一下.
方法A是变通方式不推荐,方法B是重点:
A:虽然客户的数据是利用自动计算等到的,其实也可以利用变通的方式来实现: 自动计算里是可以直接调用JS,利用JS实现精确四舍五入即可,具体写法:NumberObject.toFixed(num),num为必需,规定小数的位数:
虽然此方式可以解决,但写法比较繁琐,客户往往希望有一个简单的设置就做到了四舍五入,那就是利用设置显示格式的方式.
方法B:
1.由于报表在显示格式化的时候利用的是:
NumberFormat numberFormat = new DecimalFormat(“#0.##”); 在看过DecimalFormat源码的基础上,覆盖JAVA原有DecimalFormat的format方法,在实际进行格式化前,先对数值做好四舍五入。
2.由于显示格式里没有接口,经过和研发讨论之后,增加数值显示格式化自定义类配置项,可自行实现格式化规则.
4. 在设计器里一旦启用了数值格式化类名:填写自定义格式化类,显示格式的数值型就会利用自定义格式化规则,原本数据格式化将会失效:
5. 在设计器里具体用法,想写好的自定义格式化JAVA文件编译之后放入:
E:\reportHome\designer\web\WEB-INF\classes
在B/S模式上使用时:需要在reportConfig.xml中增加配置:
<config>
<name>numberFormatClass</name>
<value>expression.NewObjectFormat</value>
</config>
更新report4.jar
3. 资料说明
在解决四舍五入时,几经周折:
1.根据查询了相关资料得知,float和double型的底层实现是二进制的。十进制中的一个有限位数小数,转换成二进制就不一定是有限位数了,一旦位数超过的float和double型的位数宽度,就会出现”精度溢出”。所以float和double型是为了科学计算而设计的,并不适合精确的十进制计算.
2.Java中的浮点数类型的float和double是不能够用来进行资金的运算。在《Effective Java》中也提到这个原则:float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal。利用BigDecimal重新实现四舍五入部分,关键代码:
BigDecimal b = new BigDecimal(Double.toString(v));BigDecimal one = new BigDecimal(“1″);return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
BigDecimal.ROUND_HALF_UP,向”最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。
4. 程序说明
package expression;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.FieldPosition;
public class NewObjectFormat extends DecimalFormat {
private static final long serialVersionUID = 1L;
/** 要舍入的数位:个位为0,十分位为1,依次类推…… */
private int roundDigit = 0;
public NewObjectFormat(String pattern) {
super(pattern);
roundDigit = getRoundDigit(pattern);
System.out.println(“roundDigit:” + roundDigit);
}
/**
* 计算需要舍入的数位
*
* @param pattern
* @return
*/
private int getRoundDigit(String pattern) {
int digit = 0;
// 计算需要截取的位数
int index = pattern.indexOf(‘.’);
System.out.println(“INDEX:” + index);
if (index >= 0) {
for (int i = index + 1; i < pattern.length(); i++) {
char c = pattern.charAt(i);
System.out.println(“C:” + c);
if (c == ’0′ || c == ‘#’)
digit++;
else
break;
}
}
// 判断是否有百分号
if (pattern.endsWith(“%”)) {
digit += 2;
}
return digit;
}
@Override
public StringBuffer format(double number, StringBuffer result,
FieldPosition fieldPosition) {
// 将数字修正后调用父方法
return super.format(fixNumber(number), result, fieldPosition);
}
@Override
public StringBuffer format(long number, StringBuffer result,
FieldPosition fieldPosition) {
// 将数字修正后调用父方法
return super.format(fixNumber(number), result, fieldPosition);
}
private double fixNumber(double number) {
boolean isNegative = number < 0;
// 取数字的绝对值,并进行四舍五入
double n = round(Math.abs(number), roundDigit);
System.out.println(“N:” + n);
// 返回修正后的数字
return isNegative ? (0 – n) : n;
}
/**
* 提供精确的小数位四舍五入处理
*
* @param v
*需要四舍五入的数字
* @param scale
*小数点后保留几位
* @return 四舍五入后的结果,
*BigDecimal.ROUND_HALF_UP,向“最接近的“数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
“The scale must be a positive integer or zero”);
}
BigDecimal b = new BigDecimal(Double.toString(v));
BigDecimal one = new BigDecimal(“1″);
return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
public static void main(String[] args) {
double num = 4.445;
DecimalFormat df = new NewObjectFormat(“#.##”);
System.out.println(“Result:”+df.format(num));
}
}