21

突然发现个问题,我们生活中的金融系统最多只有两位小数位,比如12.37,精确到分,但是当两位小数和非整数计算时也会得出三位小数啊,比如银行的日率,肯定会有计算得出三位小数或者更多小数位的,但是为什么我们只看得到两位小数的余额呢,多出的小数位不也是钱吗,被省略了吗,怎么省略的呢。感觉这个问题有点意思。

我的猜想:

实际上只要我们金融系统只是用两位小数,当产生超过两位小数时,后面的小数位都不要了,直接不要,而不是满五进一,如果进一,就造成多给用户钱了,哪怕是多给0.001元,这在整个系统中的损失也是巨大的,所以当出现两位以上的小数时,只能直接省去后面的小数位,哪怕是0.239也要省去0.009,这个0.009元的损失只能让用户承担。

比如银行的系统,支付宝,这些都是两位小数位的,我们一般余额字段都是使用decimal(10,2),保留两位小数位,我突然想到这个问题,好纠结,不知道是不是我想的那样。

比如支付宝使用积分,购物券抵扣同时下多个订单时,那个抵扣的钱好像就是根据订单金额的比例拆分到每个订单下面去了,这样就出现小数了,但是那个小数也是两位,我没仔细注意过,不知道多个订单的抵扣加起来是不是等于下单时抵扣的钱。

如果是我想的那样,那这样我感觉很多时候我们肯定损失了好多钱了?我原本想多保留几位小数就可以解决这个问题,可是感觉可能还会有无穷的小数,还是不太可能,难道就只能让用户损失钱吗,哪怕只是那么一点点。

希望大神指点一下,搞得我好迷惑,目前项目中正遇到了这类问题。

谢谢!

xiasf 1.9k
2016-11-18 提问
1

一般系统都是保留很多位小数的吧,只不过显示一般都最多显示2位小数,存储改存储多少位就存储多少位。

杨军军 · 2017年01月11日

展开评论
18 个回答
29

已采纳

这块的资金问题还是得考虑业务需求的。按我以前的做法,是保存4位小数。根据不同的场景,进行取舍!但前台所有显示的金额都只显示2位,并向下取数

如以下场景:

余额提现、转出等


一般会向下取数,比如10.1234;那实际可提现金额为10.12

分期相关


如银行额度总共为1000元,然后刚好买了一样东西,全花了,在操作分期。分3期;
按正常思维是1000/3=333.3333333;在四舍五入一下就成了333.33;等你三期都还完了,发现只还了999.99。这就坑了

一般做法是:
前2期按四舍五入计算。最后一期,按减法算:1000-333.33-333.33=333.34

其他说明

根据不同业务,保留位数和取舍都不一样。如基金的净值。小数点的长度影响的资金量还是很大的。这块是越精准越好。具体需求跟产品沟通吧!

17

有个 银行家舍入 的方法,即:

舍去位的数值小于5时,直接舍去;
舍去位的数值大于等于6时,进位后舍去;
当舍去位的数值等于5时,分两种情况:5后面还有其他数字(非0),则进位后舍去;若5后面是0(即5是最后一位),则根据5前一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。

按上述规则举例,假设我们要求数字要求精确到个位:

49.6101 -> 50
49.499 -> 49
49.50921 -> 50
48.50921 -> 48
48.5101 -> 49

有个很好的例子来看出这种计算方式的优势:

2.55 + 3.45 = 6

如果我们要保留一位小数,去转换 2.55 和 3.45,则会变成这样:

2.6 + 3.5 = 6.1

很显然这样多了 0.1,那么按照我们的 “四舍六入五成双” 再来计算,2.55 实际上可以说是 2.550,我们要舍去的 5 的后面是 0,则根据前面一位的奇偶判断是否进位,很显然前面为奇数,则进位后为 2.6,对于 3.45,5 前面是偶数,则舍去,结果为 3.4,因此两者计算如下:

2.6 + 3.4 = 6

2016-11-24 补充说明

有人提出了 2.55 + 2.55 实际依旧多算了 0.1,而我为什么没有举这个例子呢?因为用 四舍五入,也会存在这个情况。重点是 2.45 + 2.45,常规的四舍五入会被舍去,我们需要避免这种接近中间值却未进位的情况,因为只有这样,才能保证 近似计算中舍去和进位的概率相等。这才是主要目的。毕竟只要精度有限必定存在损耗,我们只需要保证在大量样本下,概率相等即可。

1
回复 xiasf

一般应该是这样的(不过不能绝对的断定),我猜测,为什么采取这种形式主要是因为十进制是偶数进制,四舍五入 BUG 确实很明显,1234 56789 明显入的概率更高,所以应该四舍六入,那就需要对 5 单独特殊判断,所以才有了我写的:四舍六入五成双。

chongyi · 2016年11月18日

1
回复 白国栋

哈哈,高中化学老师就灌输 四舍六入五留双

liguoyi · 2016年11月18日

展开评论
4

其实并不是这样。我们电商财务系统在进行对账和对佣的时候通通算到了后6位。decimal(16, 6)在计算完之后才会四舍五入。一般最后四舍五入这一步是写在 协议里面的,最后一步我觉得应该属于协商的范畴吧。跟系统已经没啥关系了。

3

合理的均分方法可以固定小数位.
比如100元平均分成N份(保留2为小数)并计算尾差:

<?php
header('Content-Type: text/plain; charset=utf-8');
function tail($num, $fen) {
    $avg  = bcdiv($num, $fen, 2); //除
    $tail = bcsub($num, $avg*($fen-1), 2); //减
    echo $num.'='.str_repeat($avg.'+', $fen-1).$tail."\n";
    echo "$num=$avg*($fen-1)+$tail\n";
    return array($avg, $tail);
}
var_export(tail(100, 3));
var_export(tail(100, 6));
//输出:
100=33.33+33.33+33.34
100=33.33*(3-1)+33.34
array (
  0 => '33.33',
  1 => '33.34',
)
100=16.66+16.66+16.66+16.66+16.66+16.70
100=16.66*(6-1)+16.70
array (
  0 => '16.66',
  1 => '16.70',
)
1
回复 昌维

不是,bcdiv和bcsub都是PHP自带的bcmath扩展函数,类似还有GMP:
http://php.net/manual/zh/ref....
http://php.net/manual/zh/ref....

eechen · 2016年11月24日

展开评论
3

用小数存?
最好整数存分。decimal(10,2)也可以。

比2位小数更高的货币精度没有任何意义。货币是离散量,而不是连续量,它的单位就是分。法律里(CNY/RMB)不存在比分更小的货币单位,每次货币计算都不应该出现比分更小的单位。存分才合法。

并且存储系统规范不同,多位小数一不小心或将来升级,必有重大bug。

乘除法导致的6位小数?

任何对离散量的计算,都应该想办法用算法变成整数,用减法代替除法,如截尾法,银行家算法等。

例如,对100块分三期付款,不应该每次减去1/3导致精度错误,而是应该执行三次分别减去33、33、34。

例如,对102块先优惠1.8折优惠券,再优惠1.6折优惠券,不应该用102-1021.6-1021.8得到小数,而是应该用102乘1.8*1.6以四舍六入五取偶(银行家舍入)的分。因为使用优惠券不是向客户打钱,而是减免货款。

例如,提问里说的购物券抵扣钱,假设拿100块购物券,抵扣3单金额分别为20、30、50,不应该按订单金额拆分,最好在总额里汇总——财务里的借贷/出账入账概念。如果非得拆分,则只能对100做减法,先减去20块钱单的,再减去30块钱单的,最后剩下的都是50那一单的。

再次强调,RMB货币是以分为最小单位的离散量,分是货币的基本单位,不可拆分。正如人不可拆分出1.7个人。务必是序列的概念。

货币系统的可靠性,多靠财务规则完成,如果不放心,多设计一个对账系统。

2

并不是只保留两位小数。

只是面向用户只展示了两位小数,比如你的余额是 12032.12,在银行系统里的真实值可能是 12032.12593... 保留有效的小数位数由系统所需的精度要求来决定。

0

不懂很专业的东西,但是我觉得做系统没什么考虑的,两位小数后面的全部用户损失
如果进账0.111记成0.11
如果出账0.111只出0.11

0

当然不会是两位,在计算和存储都是6位及以上(有没有更多,不太清楚,但最少6位是肯定的)
只是算到最终给用户的 由于分之后无法兑现 所以就按照一定的算法计算最终结果。

0

计算机中的整数1 跟小数1.0是不相等的,所以在金钱上的小数换算为整数做处理

0

我觉得比较稳妥的方式的数据库存储的金额字段计量单位使用分,这样前台显示的时候先取整,再除100就是元了.
计算方式得分"给"和"收",以银行为主体的话,
"给":如果字段值是 200.11 那么算成元是2元零1.1厘..这个1.1厘可以舍去了,只给2元即可,就是说只取整数部分.
"收":相对可以自私一点.本着绝对不吃亏的设计原则,1厘也要算1分.那么就是字段值判断是否有小数点,有的话取整+1,没有的话直接取.如果本着四舍五入的话..各位大神就需要对有小数点的值进行判断和处理了.个人偏向绝对不吃亏原则,这个原则也应该是金融机构通用的原则.
重点时,用分做金额的计量单位,如果上亿的话,需要考虑下数据类型的Max,别超出范围了.

0

题主你居然发了金融机构秘密圈钱的手段,哈哈哈。

收款时直接把分以后的数字进位处理,付款时直接截去分以后的小数位。

0

展示显示2位小数不代表系统中处理都是2位小数吧。我觉得应该会有很多位

0

要么四舍五入,要么向下取整,保留两位

0

这种问题确实要结合实际的业务去考虑。
举个例子:
我们系统存储的金额数据都是保存四位。最初产品考虑不完善,前台各种类型的显示数据,要求全部使用四舍五入后保留两位小数,但是当系统使用一段时间后,账单方面就出现了问题,资金流动大的用户就反馈日账单数据详细数据综合与周、月账单数据对不上,月账单差距可以达到几十块不等。虽然周期结算时按照系统存储的四位小数,但是体验确实很差。

0

decimal 有银行家舍入法

0

在系统后台,对数据的记录尽可能精确,保存多位小数,用于相关计算和保存;显示给用户的时候,小数位进行取舍。

0

之前做电商项目多个商品下单,相当于多个子订单,这时候有积分抵扣需要分摊到各个子订单上面,这个时候就会遇到三位小数点的问题,计算出每个子订单均摊的积分的金额,这个时候需要去做积分总金额和均摊金额处理,比如三个子订单均分比例值,然后把多计算的或者少计算的那个金额加到或减少到最后一个商品上面。

撰写答案

你可能感兴趣的

推广链接