poi框架匯出excel寫單元格遇到精度問題
背景:
java系統,MySql資料庫,定義有些資料格式為Decimal(24,2),即最多整數22位,小數2位,或者Decimal(24,4),即最多整數20位,小數4位的數字。系統內部操作使用BigDecimal來記錄和操作這樣的資料,並無不妥,也不會丟失資料,但是當要將這樣的資料匯出的excel,問題出現了,為了便於使用者使用excel對資料(可能是金額,數量)作分析,所以我們保證單元格寫的還是數字。
以金額為例,雖然使用了formater:”#,##0.00”來格式化數字,使之顯示為形如”52,441,314.20”這個樣的字串,但這個單元格的內部value還是”52441314.20”,還是數字。
系統中,呼叫poi框架寫這樣的一個數字的程式碼如下:
cell = row.createCell(idx, CellType.NUMERIC);
cell.setCellValue(((BigDecimal) val).doubleValue());
cell.setCellStyle(cellStyles.getMoneyStyle());
第一行建立一個數字格式的單元格。
第二行是將系統內部的資料寫到單元格中去。
第三行是設定單元格的格式引用(多說一句,一個excel對一種單元格格式儘量使用同一個單元格格式變數,即儘量複用單元格格式,因為存放單元格格式是需要代價的)。
我們所使用的POI3.15僅支援下面一些setValue的方式,毫無疑問,這裡只能先將BigDecimal轉換成double再存放進去。
然後,問題開始出現了,當要寫入的val值出現形如”5244524452445244.13”這樣的大數字(18,2),在系統允許範圍內,內部計算使用BigDecimal也沒問題。但是當想要匯出到excel的時候,問題出現了,被寫入的資料是:
5.244524452445240E15
明顯的丟失了精度。經過簡單排查,很快發現了問題就是出現在了BigDecinal.doubleValue()這個方法上面。
BigDecimal | 5244524452445244.13 |
---|---|
Double | 5.244524452445244E15 |
正是Double的值成了科學計數法提醒了我,雖然double型別可以具有寬闊的表值空間:-1.7*10(-308)~1.7*10(308)。但是它能維持的精度可能沒有這裡需要的高。
double結構分析:
看一下double的格式,一個double數是64位,它分為三個區域:
1 | 11 | 52 |
---|---|---|
符號位 | 指數位 | 尾數位 |
簡單舉一個例子,十進位制數字9二進位制是:1001,
十進位制數字0.625的二進位制是:0.101;
所以十進位制數字9.625的十進位制數是1001.101,也就是1.001101*2^3。double是移位儲存(這個概念不清楚請自行百度)的,所以這個數字存放進double就是:
符號位 | 指數位 | 尾數位 |
---|---|---|
0 | 1…100 | 0011010…0 |
63 | 62…52 | 51…0 |
因為移位計算後,科學計數法首位都是1,不用存,所以double 52位的尾數能表達出53位的精度。
範圍
11bit的指數位的表達區間是-2^10到2^10
所以double的表值範圍是:
這個數字範圍是如此的大,已經夠接近“近似無窮大”的實際使用範圍了。估算一下:
故double的表達範圍是:
-
精度:
考慮到基數必然實在1到2之間,故double的實際精度就是E15~E16.也就是說,double最多可以表達的精度是十進位制的15~16位,這個意思是說double用來表達十進位制數,有效位數最多可以是15位或者16位。
結論
而這裡我們的數字是”5244524452445244.13”,有效數字位為18尾,所以觸發了末尾精度的丟失,轉換成double之後,記錄下的結果是:5.244524452445244E15,丟了兩位精度,只留下了高位的16位有效數字。
後記
當然,我們可以直接向excel中寫入BigDecimal.toString()的format後的字串,但是這樣丟失了“數字”這一單元格型別屬性,在使用者需要進行資料分析的時候,還是需要將其轉化成數字型別,這樣還是會受到有效數位的限制,依舊會丟失精度。和專案經理溝通之後,我們還是選擇了忽略這種精度丟失問題Decimal(15,2)已經能表達萬億級別的金額,實際業務場景中,基本上不會出現需要匯出這樣大的金額的情況,權衡之下,還是情願損失有效數位過長時候的精度,而保留匯出的單元格是數字型別。