V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
GrapeCityChina
V2EX  ›  推广

“银行家算法”大揭秘!在前端表格中利用自定义公式实现“四舍六入五成双”

  •  
  •   GrapeCityChina · 2022-05-11 13:45:29 +08:00 · 1704 次点击
    这是一个创建于 961 天前的主题,其中的信息可能已经有所发展或是发生改变。

    银行的盈利模式是什么?三个字:信息差!从储户手中收拢资金,然后放贷出去,而所谓的“利润”就是这其中的利息差额。 在我国,人民银行规定每个季度月末的 20 号为银行结息日,每一年四次结息,因此每年需要非常频繁的计算付给储户的利息。在计算利息时,小数点如何处理就变得很重要,并成为决定利润多少的关键细节。

    (图片来自于网络)

    通常,我们都知道在保留小数点的时候,常常会用到四舍五入。小于 5 的数字被舍去,大于等于 5 的数字进位后舍去,由于所有位上的数字都是自然计算出来的,按照概率计算可知,被舍入的数字均匀分布在 0 到 9 之间。 我们不妨以 10 笔存款利息计算作为模型,以银行家的身份来思考这个算法:

    四舍,舍弃的值包含:0.000 、0.001 、0.002 、0.003 、0.004 ,对银行而言舍弃的内容就不再需要支付,所以舍弃的部分我们可以理解为“赚到了”。

    五入,进位的内容包括:0.005 、0.006 、0.007 、0.008 、0.009 ,对银行而言进位内容会造成亏损,对应亏损的金额则是:0.005 、0.004 、0.003 、0.002 、0.001 。 因为舍弃和进位的数字是在 0 到 9 之间均匀分布的,所以对于银行家来说,每 10 笔存款的利息因采用四舍五入而获得的盈利是: 0.000 + 0.001 + 0.002 + 0.003 + 0.004 - 0.005 - 0.004 - 0.003 - 0.002 - 0.001 = -0.005 总体来讲每 10 笔的利息,通过四舍五入计算就会导致 0.005 元的损失,即每笔利息计算损失 0.0005 元。假设某家银行有 5 千万储户,每年仅仅因为四舍五入的误差而损失的金额是:

    public class Client {  
         public static void main(String[] args) {  
              //银行账户数量,5 千万  
              int accountNum =5000*10000;  
              //按照人行的规定,每个季度末月的 20 日为银行结息日  
              double cost = 0.0005 * accountNum * 4 ;  
              System.out.println("银行每年损失的金额:" + cost);  
         }  
    } 
    
    

    计算结果是:“银行每年损失的金额:100000.0”。你可能难以相信,四舍五入小小一个动作,就导致了每年损失 10 万。但在真实环境中,实际损失可能事更多。 这个情况是由美国的私人银行家发现,为了解决这一情况提出了一个修正算法: “舍去位的数值小于 5 时,直接舍去; 舍去位的数值大于等于 6 时,进位后舍去; 当舍去位的数值等于 5 时,分两种情况:5 后面还有其他数字(非 0 ),则进位后舍去;若 5 后面是 0 (即 5 是最后一个数字),则根据 5 前一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。” 以上这么多,汇成一句话就是:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。 我们举例说明,取 2 位精度: 10.5551= 10.56
    10.555= 10.56
    10.545= 10.54

    (图片来自于网络)

    简单来说,有了“四舍六入五成双”这样的银行家算法,就可以更为科学精确地处理数据。

    在实际应用中,我们使用银行家算法最多的情况就是在大数据量的表格计算中,但是在表格计算中需要通过一系列的内置公式进行复合。对于普通用户来说无论是理解还是最终使用,都很繁琐且复杂。 为了更加方便地解决这个问题,我们可以通过自定义函数来完成这样的需求,这样用户只需要记住自定义的函数名即可使用具有这样一个规则的函数。 接下来我们一起看看,如何在前端表格中快速地实现“四舍六入五成双”。 我们首先需要定义函数的名称,以及里面的参数数目。因为我们想要实现的是,传递两个参数,“1”是需要被约修的数值,“2”是保留小数点后面的位数,根据值和位数进行约修。

    var FdaFunction = function() {
                 this.name = "FDA";
                 this.minArgs = 1;
                 this.maxArgs = 2;
             };
    
    

    接下来就是为了方便用户理解和使用,我们需要对这个自定义函数添加一些描述:

     FdaFunction.prototype.description = function() {
                 return {
                     description: "对 value 进行四舍六入五留双修约,保留小数点后指定位数",
                     parameters: [{
                         name: "value",
                         repeatable: false,
                         optional: false
                     }, {
                         name: "places",
                         repeatable: false,
                         optional: false
                     }]
                 }
             }
    
    

    最后到了关键步骤,也就是函数的逻辑运行都放在 evaluate 中,我们会对传入的值做一些判断,并且会利用正则表达式做一些匹配。要实现“五成双”,那么我们还要对需要约修的最后一个位值做判断,来决定是否进位。具体可以参考附件完整的 demo 。

             FdaFunction.prototype.evaluate = function(context, num, places) {
    
                if (!isNaN(parseInt(num)) && !isNaN(parseInt(places))) {
                    console.log("evaluate")
                     num = numGeneral(num);
                    if (!isNumber(num)) {
                        return num;
                    }
                    var d = places || 0;
                    var m = Math.pow(10, d);
                    var n = +(d ? num * m : num).toFixed(8); // Avoid rounding errors
                    var i = Math.floor(n),
                        f = n - i;
                    var e = 1e-8; // Allow for rounding errors in f
                    var r = f > 0.5 - e && f < 0.5 + e ? (i % 2 == 0 ? i : i + 1) : Math.round(n);
                    var result = d ? r / m : r;
    
                    if (places > 0) {
                        var s_x = result.toString();
                        var pos_decimal = s_x.indexOf(".");
                        if (pos_decimal < 0) {
                            pos_decimal = s_x.length;
                            s_x += ".";
                        }
                        while (s_x.length <= pos_decimal + places) {
                            s_x += "0";
                        }
                        return s_x;
                    } else {
                        return result;
                    }
                }else{
                     return "#VALUE!";
                }
               
             }
    
    

    体验下载完整 demo: https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MTkyNjA3fDQzMTk2ZmRhfDE2NTAyNzI0NTV8NjI2NzZ8MTQxNjY4

    大家如果想了解更多与自定义公式相关内容,可以查看链接: https://demo.grapecity.com.cn/spreadjs/SpreadJSTutorial/features/calculation/custom-functions/purejs

    6 条回复    2022-05-12 19:27:00 +08:00
    xQmQ
        1
    xQmQ  
       2022-05-11 15:45:51 +08:00 via iPhone   ❤️ 1
    我以为是避免死锁那个银行家算法,搞错了
    weyou
        2
    weyou  
       2022-05-12 14:05:16 +08:00 via Android
    Python 的 round()就是“四舍六入五成双”啊
    vikaptain
        3
    vikaptain  
       2022-05-12 15:20:26 +08:00
    toFixed 方法就是这种实现
    shakoon
        4
    shakoon  
       2022-05-12 16:26:27 +08:00
    作为银行从业者,表示若干年前金融业全面电子化后,因为四舍五入、汇率转换等情况产生的细微数据偏差,很多年就已经有成熟的处理方式了,这个并不是新鲜事。而在四舍六入五成双广泛使用前,任何因为转换产生的损益也全都有记录,并且在记账时有专门的会计科目来记录的。
    不要把资本家想得太简单→_→
    opengps
        5
    opengps  
       2022-05-12 16:30:42 +08:00
    神奇的经历:上午刚因为 real 类型接触到 10 的 42 次方,搜资料看到“银行家算法”这个词,这会就在这里看到帖子
    IvanLi127
        6
    IvanLi127  
       2022-05-12 19:27:00 +08:00 via Android
    toFixed 啊
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2491 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 01:29 · PVG 09:29 · LAX 17:29 · JFK 20:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.