傳統的四捨五入
首先,讓我們看幾個傳統四捨五入的例子:
四捨五入到小數第一位:
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
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 算錯呢?
官方文件指出 RoundTo 使用的是「Banker's rounding(銀行家捨入法)/四捨五入六成雙」。這種方法的規則依據 Wiki 上寫道:
- 保留位數的後一位如果是4,則捨去。
- 保留位數的後一位如果是6,則進位。
- 保留位數的後一位如果是5,而且5後面不再有數,要根據尾數5的前一位決定是捨去還是進入:如果是奇數則進入,如果是偶數則捨去。
- 要求保留位數的後一位如果是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」平台上浮點數運算的差異所導致些微的不同。而這些差異源於幾個因素:
另外,當您需要一個在 Win32 和 Win64 平台都適用的四捨五入函數時,我在 StackOverflow【點我前往】找到網友提供的另一種解決方案。以下為該網友提供的四捨五入方法:- 浮點表示和精度: 浮點數在電腦中的表示和精度可能因平台(32位元和64位元)而異。32位元和64位元架構使用不同的方式來處理浮點數的表示和計算,這可能導致即使是相同的浮點運算在不同平台上產生微小的差異。
- 編譯器和浮點數庫的差異: Delphi 在不同的架構下可能會使用不同的編譯器優化設置或浮點數函式庫,這可能會對浮點數運算的結果產生影響。
- 浮點運算的內部處理: 浮點運算在不同的處理器架構下可能會有不同的內部處理方式。例如,某些浮點運算可能在一個平台上使用硬體加速,而在另一個平台上則使用軟體模擬。
- 中間值的舍入規則: 在接近中間值(如 0.5 或其它類似的數值)的情況下,不同的浮點數處理方法可能會導致不同的舍入行為。
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
0 Comments
張貼留言