當我們從小學習四捨五入時,通常遵循的原則是「逢五進位」。然而,在程式設計中,四捨五入的結果經常與我們的預期不符,導致客戶常常反應四捨五入數值計算錯誤的問題發生。今天,讓我們來探討一下四捨五入背後的一些細節知識吧!



傳統的四捨五入

首先,讓我們看幾個傳統四捨五入的例子:
四捨五入到小數第一位:
2.45 = 2.5
2.452 = 2.5
3.55 = 3.6
3.557 = 3.6

四捨五入到小數第二位
4.225 = 4.23
4.2256 = 4.23
6.365 = 6.37
6.3652 = 6.37
當四捨五入到小數第一位時,例如2.45變成2.5,而3.55則變成3.6。同樣地,當四捨五入到小數第二位時,4.225變成4.23,6.365則變成6.37。在這些情況下,我們可以直觀地看到逢五即進位的規則。



Banker's Rounding 銀行家捨入法/四捨五入六成雙

然而,當我們在 Delphi 中使用 Round 函數時,情況就變得有趣了!以下為程式碼和結果:
ShowMessage(RoundTo(2.45, -1).ToString); //Answer = 2.4
ShowMessage(RoundTo(2.452, -1).ToString); //Answer = 2.5
ShowMessage(RoundTo(3.55, -1).ToString); //Answer = 3.6
ShowMessage(RoundTo(3.557, -1).ToString); //Answer = 3.6
ShowMessage(RoundTo(4.225, -2).ToString); //Answer = 4.22
ShowMessage(RoundTo(4.2256, -2).ToString); //Answer = 4.23
ShowMessage(RoundTo(6.365, -2).ToString); //Answer = 6.36
ShowMessage(RoundTo(6.3652, -2).ToString); //Answer = 6.37
用 RoundTo 函數時,2.45變成了2.4,但2.452卻變成了2.5?修但幾勒!怎麼跟認知上的不同呢?是不是 Delphi 算錯呢?

先不用緊張,讓我們來看看 Delphi 的官方文件說明。

官方文件指出 RoundTo 使用的是「Banker's rounding(銀行家捨入法)/四捨五入六成雙」。這種方法的規則依據 Wiki 上寫道:
  1. 保留位數的後一位如果是4,則捨去。
  2. 保留位數的後一位如果是6,則進位。
  3. 保留位數的後一位如果是5,而且5後面不再有數,要根據尾數5的前一位決定是捨去還是進入:如果是奇數則進入,如果是偶數則捨去。
  4. 要求保留位數的後一位如果是5,而且5後面仍有數,則無論奇偶都要進入。
所以2.45因為第三點的規則,前一位4是偶數,2.45直接捨去變成2.4。而2.452時,因為第四點的規則,所以無論如何都要進位,因此答案是2.5。

那麼,為什麼會有 Banker's rounding(銀行家捨入法)/四捨五入六成雙呢?原因在於,在大量計算時,傳統的四捨五入方法會產生偏差,尤其是在統計學中。傳統方法由於總是向上進位,可能導致增加的系統性誤差。相比之下,Banker's rounding(銀行家捨入法)/四捨五入六成雙在統計上更為中性,有助於避免這種偏差。
💡 電機電子工程協會(IEEE)也將此方法設為四捨五入法的標準(同 IEEE 754【點我前往】)。



如何調整為傳統的四捨五入

Delphi的System.Math庫中提供了一個名為「SimpleRoundTo」的函數,這個函數根據官方文檔的說明,實現的是我們傳統所認知的四捨五入方法。

「Banker's rounding(銀行家捨入法)/四捨五入六成雙」相比,SimpleRoundTo函數遵循較為直觀的進位規則:當數值的某一位數在四捨五入時遇到5,無論其後是否還有其他數字,都會進位。這使得 SimpleRoundTo 成為一個適用於日常計算需求的工具,尤其是在那些不需要考慮統計學上的偏差微調的場景中。
//Win32
ShowMessage(SimpleRoundTo(2.45, -1).ToString); //Answer = 2.5
ShowMessage(SimpleRoundTo(2.452, -1).ToString); //Answer = 2.5
ShowMessage(SimpleRoundTo(3.55, -1).ToString); //Answer = 3.6
ShowMessage(SimpleRoundTo(3.557, -1).ToString); //Answer = 3.6
ShowMessage(SimpleRoundTo(4.225, -2).ToString); //Answer = 4.23
ShowMessage(SimpleRoundTo(4.2256, -2).ToString); //Answer = 4.23
ShowMessage(SimpleRoundTo(6.365, -2).ToString); //Answer = 6.37
ShowMessage(SimpleRoundTo(6.3652, -2).ToString); //Answer = 6.37

//Win64
ShowMessage(SimpleRoundTo(2.45, -1).ToString); //Answer = 2.5
ShowMessage(SimpleRoundTo(2.452, -1).ToString); //Answer = 2.5
ShowMessage(SimpleRoundTo(3.55, -1).ToString); //Answer = 3.5
ShowMessage(SimpleRoundTo(3.557, -1).ToString); //Answer = 3.6
ShowMessage(SimpleRoundTo(4.225, -2).ToString); //Answer = 4.22
ShowMessage(SimpleRoundTo(4.2256, -2).ToString); //Answer = 4.23
ShowMessage(SimpleRoundTo(6.365, -2).ToString); //Answer = 6.37
ShowMessage(SimpleRoundTo(6.3652, -2).ToString); //Answer = 6.37
不過經過測試後,SimpleRoundTo 函數會因為「Win32」「Win64」平台上浮點數運算的差異所導致些微的不同。而這些差異源於幾個因素:
  1. 浮點表示和精度: 浮點數在電腦中的表示和精度可能因平台(32位元和64位元)而異。32位元和64位元架構使用不同的方式來處理浮點數的表示和計算,這可能導致即使是相同的浮點運算在不同平台上產生微小的差異。
  2. 編譯器和浮點數庫的差異: Delphi 在不同的架構下可能會使用不同的編譯器優化設置或浮點數函式庫,這可能會對浮點數運算的結果產生影響。
  3. 浮點運算的內部處理: 浮點運算在不同的處理器架構下可能會有不同的內部處理方式。例如,某些浮點運算可能在一個平台上使用硬體加速,而在另一個平台上則使用軟體模擬。
  4. 中間值的舍入規則: 在接近中間值(如 0.5 或其它類似的數值)的情況下,不同的浮點數處理方法可能會導致不同的舍入行為。
另外,當您需要一個在 Win32 和 Win64 平台都適用的四捨五入函數時,我在 StackOverflow【點我前往】找到網友提供的另一種解決方案。以下為該網友提供的四捨五入方法:
function SimepleRoundTo2(const AValue: Double; const ADigit: Integer): Double;
var
  LFactor: Extended;
begin
  LFactor := IntPower(10.0, ADigit);
  if AValue < 0 then
    Result := Int((AValue / LFactor) - (0.500000001)) * LFactor
  else
    Result := Int((AValue / LFactor) + (0.500000001)) * LFactor;
end;

ShowMessage(SimepleRoundTo2(2.45, -1).ToString); //Answer = 2.5
ShowMessage(SimepleRoundTo2(2.452, -1).ToString); //Answer = 2.5
ShowMessage(SimepleRoundTo2(3.55, -1).ToString); //Answer = 3.6
ShowMessage(SimepleRoundTo2(3.557, -1).ToString); //Answer = 3.6
ShowMessage(SimepleRoundTo2(4.225, -2).ToString); //Answer = 4.23
ShowMessage(SimepleRoundTo2(4.2256, -2).ToString); //Answer = 4.23
ShowMessage(SimepleRoundTo2(6.365, -2).ToString); //Answer = 6.37
ShowMessage(SimepleRoundTo2(6.3652, -2).ToString); //Answer = 6.37



總結

當我們回想起學校裡學的四捨五入,那個逢五就進位的規則總是那麼簡單明瞭。但說到程式設計,事情就變得有點不一樣了,尤其當我們跨越不同的計算平台。有時候,這些四捨五入的結果會讓人意外,解釋給客戶聽,他們甚至覺得我們在開玩笑呢!所以,搞懂四捨五入在傳統數學和我們開發的語言所規範的四捨五入是怎麼運作的,就變得挺重要的哦!