CODE-分贓程式的寫法
把一筆錢依特定的比例分給幾個人是我工作上常要處理的需求。由於金額必須四捨五入到元或分,因此常需面對除不盡的錢要設法攤掉的問題。例如100元平分給三個人,每人33元後,最後的1元要發給三人之一的幸運兒,變成一人34, 兩人33的分配結果。
以前年紀小不懂事,很直覺的想法是先用100*1/3四捨五入得到33把錢分一分,之後再跑一個迴圈(沒辦法,總不能打電話請這三個人過來猜拳吧?)把分剩的錢(總金額大、人數多時餘下數十上百元也是有可能滴)每次一元地發下去,直到發光為止。
說實在說,當初並不覺得這個寫法有什麼不對,直到有前輩指點了另一種更精巧的演算法,一口氣就能把錢攤到一毛不剩,省去分完一輪後處理餘數的麻煩。相形之下,原本的寫法挺笨拙的...
using System;
using System.IO;
using System.Threading;
public class CSharpLab
{
public static void Test()
{
//100萬元要依比例分給3個人(四拾五入計算到元)
int totalAmt = 1000000;
int[] amt = new int[3];
//三個人勢均力敵,100萬除以3會有餘1元的問題
//且看以下的邏輯演法如何避免事後分攤的困擾
decimal[] fact = new decimal[] { 1.2M, 1.2M, 1.2M };
//先加總分配權重
decimal factSum = 0;
for (int i = 0; i < fact.Length; i++)
factSum += fact[i];
//使用以下邏輯分配, 會自然吸收掉四捨五入差額
for (int i = 0; i < fact.Length; i++)
{
amt[i] = Convert.ToInt32(
Math.Round(totalAmt * fact[i] / factSum, MidpointRounding.AwayFromZero)
);
Console.WriteLine("{0} * {1} / {2} = {3}", totalAmt, fact[i], factSum, amt[i]);
totalAmt -= amt[i];
factSum -= fact[i];
}
}
}
執行結果是: (以上程式可以用Mini C# Lab直接測試)
1000000 * 1.2 / 3.6 = 333333
666667 * 1.2 / 2.4 = 333334
333333 * 1.2 / 1.2 = 333333
很棒吧! 它可以很自然順暢地在分攤過程中吸收掉四捨五入可能產生的差額,一步到位!
最近手上的案子在寫AJAX網頁,開始把這樣的概念搬到Javascript上實作。(以下程式可以使用Mini jQuery Lab測試)
var factor = [ 1.2, 1.2, 1.2 ], factorSum = 0;
var totalAmount = 1000000, amount = [];
for (var i = 0; i < factor.length; i++) factorSum += factor[i];
var r = "";
for (var i = 0; i < factor.length; i++)
{
amount[i] = parseInt(Math.round(totalAmount * factor[i] / factorSum))
r += totalAmount + " * " + factor[i] + " / " + factorSum + " = " + amount[i] + "\n";
totalAmount -= amount[i];
factorSum -= factor[i];
}
alert(r);
執行結果是
1000000 * 1.2 / 3.5999999999999996 = 333333
666667 * 1.2 / 2.3999999999999994 = 333334
333333 * 1.2 / 1.1999999999999995 = 333333
數字是對的,但明眼人已可看到其中暗藏殺機... 3.5999999999999996? 明明應該是3.6,因為Javascript裡只有浮點數,沒有像.NET decimal一樣分亳不差的精準數字型別,所以數字都是用逼近的。
俗話說得好: "算錢用浮點,遲早被人扁"! 雖然浮點運算的微小誤差多半要遇到天文數字計算或複雜的累乘累除時才會爆炸,但實事求事總是比較好,跟錢有關的東西,一丁點不對都會吵翻天。"插擠摳"(差一元)是每一個帳務會計程式開發人員的惡夢,為了避免未來某一天為了找一塊錢找到吐血,這裡還是花點心思防範未然。
var factor = [1.2, 1.2, 1.2], factorSum = 0;
var totalAmount = 1000000, amount = [];
function r2(n) { return parseFloat(n.toFixed(2)); }
for (var i = 0; i < factor.length; i++)
factorSum = r2(factorSum + factor[i]);
var r = "";
for (var i = 0; i < factor.length; i++)
{
amount[i] = parseInt(Math.round(totalAmount * factor[i] / factorSum))
r += totalAmount + " * " + factor[i] + " / " + factorSum + " = " + amount[i] + "\n";
totalAmount -= amount[i];
factorSum = r2(factorSum - factor[i]);
}
alert(r);
我假設factor的精準度到兩位,利用to.Fixed(2)的方法四捨五入加總及刪減過的結果。經過這番修正,結果好看多了!
1000000 * 1.2 / 3.6 = 333333
666667 * 1.2 / 2.4 = 333334
333333 * 1.2 / 1.2 = 333333
【心得】Javascript在數字處理上挺弱的,沒有decimal,型別不嚴謹,計算速度無法跟.NET等編譯式語言匹敵,連四捨五入到小數第幾位都得繞一圈。不過看在它可以輕易跨平台跨瀏覽器的分上,只好摸摸鼻子認了。Silverlight 3.0正式版快要誕生了,過陣子再來Survey它與AJAX網頁應用上的互補性。
[2012-02-23更新]本演算法在極端例子下會崩壞,修正方法請參見補充文章。