Quantcast
Channel: 黑暗執行緒
Viewing all articles
Browse latest Browse all 2311

關於 Decimal 小數尾數零

$
0
0

C#的實數型別有三種:float、double、decimal。其中 float、double 為浮點數,本站的老讀者們一定知道-「算錢用浮點,遲早被人扁」的道理,因此只要涉及金額計算,我一律改用 decimal 型別。前幾天,踩到 decimal 小數尾數零地雷一枚。

以下程式為例,大家猜猜結果為何?

class Program
    {
 
staticvoid Main(string[] args)
        {
float flt = 1.2300F;
            Console.WriteLine(flt);
double dbl = 1.2300D;
            Console.WriteLine(dbl);
decimal dcm = 1.2300M;
decimal cmp = 1.23M;
            Console.WriteLine(dcm == cmp);
            Console.WriteLine($"{dcm} vs {cmp}");
            Console.Read();
        }
    }

註:實數常值(Real Literal)後方需加上尾碼以明確宣告型別,F f 代表 float、D d 或不加尾碼代表 double、M m 則代表 decimal。

由結果可知,浮點數型別(float、double)不會保留小數尾數零,decimal 則可做到 1.23 與 1.2300 有別:進行 = 比對二者相等,但 ToString() 時小數尾數零可忠實還原。

事情發生在一段分別由 SQL 及 Oracle 取出對應資料比對的程式,目的在驗證兩邊資料庫是否一致。為簡化比對邏輯,我將所有欄位內容 ToString() 轉為字串,心想 SQL 與 Oracle 的 Schema 一致,欄位都是 DECIMAL(5,4) 且儲存數值相同,豈有寫入 decimal 後 ToString() 結果不同的道理?結果,真的被我遇上了。

用以下範例重現問題,在 SQL 與 Oracle 端分別將 1.23 存入 DECIMAL(5,4),再用 Dapper 讀取到 decimal:

staticvoid Main(string[] args)
        {
using (var cn = new SqlConnection(csSql))
            {
decimal d = cn.Query<decimal>(
@"
declare @d decimal(5,4);
set @d=1.23;
select @d;").Single();
                Console.WriteLine(d);
            }
using (var cn = new OracleConnection(csOra)) 
            {
                cn.Open();
                cn.Execute(
@"create global temporary table decimal_test (d decimal(5,4)) on commit preserve rows");
                cn.Execute("insert into decimal_test values (1.23)");
 
decimal d = cn.Query<decimal>("select d from decimal_test").First();
                Console.WriteLine(d);
                cn.Execute("truncate table decimal_test");
                cn.Execute("drop table decimal_test");
 
            }
            Console.Read();
        }

結果分別為 1.2300 與 1.23。SQL Client 讀取 DECIMAL(5,4) 轉入 decimal 時後方會補足 0 到精確位數,而 ODP.NET 不會,二者的行為差異造成讀取 decimal 的小數尾數零數目不同,不影響大於小於等於比對,遇到 ToString() 轉字串,便會得到不同結果。

至於要怎麼去除 decimal ToString() 夾帶的尾數零,有幾種做法

  1. 如果確定要保留小數位數上限,可以寫成 ToString("#.####")。缺點是若 # 個數小於實際小數位數會被四捨五入影響精確度。若求保險,小數點後寫上 28 個 # 肯定安全。(decimal 精確度上限為 29 位)
  2. 用 ToString("G29") 轉為科學記號,但要求 29 位精準位數,成為位數足又不會出現 E 的幾次方的偽科學計號。缺點是 0.00001 這類微小數會被轉成 1E-05。
  3. 在 Stackoverflow 看到奇妙解法,decimal 除上 1.00000…(29個0):
    publicstaticdecimal Normalize(thisdecimalvalue)
    {
    returnvalue/1.000000000000000000000000000000000m;
    }
       
    寫成擴充方法,1.2300m.Normalize() 尾數零就會清光光。

簡要心得

  • decimal 會保存小數尾數零,float、double 等浮點型別不會
  • 小數尾數零不影響大於等於小於比對,但會影響 ToString() 結果
  • SQL Client 與 ODP.NET 讀取 DECIMAL(m, n) 欄位寫入 decimal 時對於小數尾數零的處理原則不同
  • 去除 decimal 小數尾數零有幾種做法:ToString("#.####")、ToString("G29") 以及奇妙的 Normalize() 方法
  • 政令宣導時間:「算錢用浮點,遲早被人扁」。早晚複誦,永誌不忘~

Viewing all articles
Browse latest Browse all 2311

Trending Articles