zoukankan      html  css  js  c++  java
  • 财务模块中的金额字段类型设置

      【浮点类型计算的误差】

      在财务模块的设计中,一定会涉及到金额的处理,其中字段类型的设计很关键,如果采用了float和double类型,计算结果会有误差。

            float i = 1.1f;
            float j = 1f;
            System.out.println(i - j);
            //0.100000024    

      所以,在涉及到要求精度精确的金额时,一般会采用decimal类型存储在DB中,而Java计算的过程则采用BigDecimal。

      【历史包袱】

      在一些早期的财务软件中,或者很多初期没有考虑这方面问题的软件中,金额类的字段在数据库中也会被设置为float。如金蝶、用友等企业的早期产品往往也是用float进行金额数据的存储。但这样还怎么保证数据的准确性呢?

      其实相当多的系统设计中,系统怎么去存储数据和怎么去计算数据并不统一。有的会以float存储,但是计算的时候还是会采用decimal的转换,再进行计算,虽然也能保证结果的准确性,但还是很麻烦;另外,在使用MySQL的自带函数时会有一些麻烦,还涉及到类型的转换(未尝试,读者可以自行验证)。也就是说,存储数据用什么格式,看具体的场景,可以是int,可以是float,但是只要计算过程保证使用了decimal,就保证了计算的精度。

      【为什么是decimal】

      在设计实体类型时,我们用BigDecimal定义字段,数据库中以decimal存储,那么存取数据,计算过程都统一这一种类型。不必在取出来时,再进行一次类型转换,避免了很多出bug的可能;如果是Integer型或者Long存以分为单位的数据,那么存取,展示(通常是以'元'为单位)的时候都要进行单位转换,这样也很容易出错;另外,在生产过程中,通常也会有一些场景,管理人员会直接从数据库中导出数据,不经过系统拿到数据,单位是分的话是不符合财务人员的使用习惯的,这人为地增加了沟通成本。所以综合来看,decimal是一个比较好的选择,也是业界常规的做法。

      【关于误差】

      上面讨论了字段类型和精度的关系,这里要说明的是财务数据的误差。财务数据的计算结果有误差是避免不了的,所以财会领域有一个专业词汇叫做调账,调账就是为了调整误差带来的账目差异。

      但是,我们的代码还是要保证整个计算结果的精确,这里要表达的是,就算是有误差,那么这个计算结果的误差,也应该同财务人员计算结果的误差保持一致。为了达到这个目的,我们需要保证2点:

      1.在关键的,会出现误差的计算过程中,精度与财务人员的要求保持一致;

      2.整个计算的步骤,也要与财务人员的计算步骤保持一致。

      关于第2点,并不是说,理论上和逻辑上与这个流程一致就行,而是要在步骤上一致。这是因为步骤不同,就算逻辑处理是等价的,最终的计算结果产生的误差肯定不会一样,这就违背了刚刚说明的原则。

      比如10笔订单,每笔订单收益1元,而某个代理商收益为30%。计算过程如果是先进行单笔计算,计算完汇总,保留2位小数,那么结果是3.30;但如果是先进行汇总,再进行收益计算,结果是3.33。当订单数据巨大时,这个收益的误差也会是巨大的。

      当然,如果是考虑计算效率,存储效率等问题,需要对这个计算过程进行调整,只要财务人员接受由此带来的误差,也是OK的。

      3.以上两点是基于技术与业务来说的,这一点是基于商业上的考虑。在对用户进行收费的过程中,计算的步骤,计算的精度保留,所带来的误差,如果商家的算法设置的合理,可以通过合理的舍或进的手段获取到由于计算精度带来的额外收益。在这个过程中,用户其实是无感的,比如一笔订单,用户的损失可能不足1分钱,但是对于商家来说,订单数量特别大时,这个额外的收益也是很可观的。

      【Java中的BigDecimal】

      基本用法:

        public static void main(String[] args) {
            BigDecimal number1 = new BigDecimal(0.005);
            BigDecimal number2 = new BigDecimal(1000000);
    
            BigDecimal stringParseNumber1 = new BigDecimal("0.005");
            BigDecimal stringParseNumber2 = new BigDecimal("1000000");
    
            //加法
            BigDecimal result = number1.add(number2);
            BigDecimal stringParsedResult = stringParseNumber1.add(stringParseNumber2);
    
            System.out.println("result=" + result);
            System.out.println("stringParsedResult=" + stringParsedResult);
    
            //减法
            BigDecimal subtract = stringParseNumber1.subtract(stringParseNumber1);
            //乘法
            BigDecimal multiply = stringParseNumber1.multiply(stringParseNumber2);
            //绝对值
            BigDecimal abs = stringParseNumber1.abs();
    
            //除法:必须制定小数后的精确位数,以及进位的原则
            BigDecimal divisor = new BigDecimal("10");
            BigDecimal dividend = new BigDecimal("3");
            BigDecimal divideResult = divisor.divide(dividend, 3, BigDecimal.ROUND_HALF_UP);
            System.out.println(divideResult);
        }

      由以上结果中,可以看到,如果构造函数中传入的是double或者float类型,那么BigDecimal的计算结果还是不准确。这里推荐使用String类型作为构造函数的参数。

      对于除法,第二个参数表示计算结果小数保留位数,第三个参数表示进位的算法。这里列举所有的算法,在实际场景中可以根据需求选用:

    ROUND_UP //对非舍去的部分始终在保留的最低位加1
    ROUND_DOWN //与ROUND_UP相反,始终不对非舍去的部分加1
    ROUND_CEILING //如果为正,则舍入原则与ROUND_UP一致,如果为负,则舍入原则与ROUND_DOWN一致 ROUND_FLOOR //与CEILING刚好相反 可以将这两种模式理解为坐标轴的方向
    ROUND_HALF_UP //四舍五入 ROUND_HALF_DOWN //五舍六入 ROUND_HALF_EVEN //银行家舍入法,主要在美国使用。四舍六入是肯定的,五分为两种情况,前一位为奇数,则入位,前一位为偶数,则舍去。 ROUND_UNNECESSARY //断言使用

      这里给出实际的使用场景,体会一下进位的结果:

            System.out.println(new BigDecimal("0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_UP));//0.121
            System.out.println(new BigDecimal("-0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_UP));//-0.121
            System.out.println(new BigDecimal("0.1891").divide(new BigDecimal("1"),3,BigDecimal.ROUND_UP));//0.190
            System.out.println(new BigDecimal("0.91").divide(new BigDecimal("1"),1,BigDecimal.ROUND_UP));//1.0
    
            System.out.println(new BigDecimal("0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_DOWN));//0.120
            System.out.println(new BigDecimal("-0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_DOWN));//-0.120
            System.out.println(new BigDecimal("0.1891").divide(new BigDecimal("1"),3,BigDecimal.ROUND_DOWN));//0.189
            System.out.println(new BigDecimal("0.91").divide(new BigDecimal("1"),1,BigDecimal.ROUND_DOWN));//0.9

  • 相关阅读:
    课后作业07--二分法查找算法代码
    检索03 -- 二分法查找
    课堂随笔05--冒泡排序
    课后作业 06 -- 小数后几位精确判断并输出
    课后作业 05 -- 输出一段带字母与数字的随机数
    课后作业 04 --DateTime应用,判断多久后生日之类
    检索02--随机数种子的一些概念和理解
    课堂随笔04--关于string类的一些基本操作
    P1174 互素
    P1001 第K极值
  • 原文地址:https://www.cnblogs.com/bruceChan0018/p/14967683.html
Copyright © 2011-2022 走看看