“銀行家演算法”大揭祕!在前端表格中利用自定義公式實現“四捨六入五成雙”

語言: CN / TW / HK

銀行的盈利模式是什麼?三個字:資訊差!從儲戶手中收攏資金,然後放貸出去,而所謂的“利潤”就是這其中的利息差額。

在我國,人民銀行規定每個季度月末的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...

大家如果想了解更多與自定義公式相關內容,可以檢視連結:

https://demo.grapecity.com.cn...

擴充套件閱讀

SpreadJS 純前端表格控制元件