【大數字精確計算】當心你的float、double類型產生誤差

遇到一個Bug

需求:在頁面上顯示一個產品的已購百分比

已購百分比 = ((總量 - 剩余數量)/總量) * 100%
顯示結果向下取整(取兩位有效數字)画切,
例如60.1% 則取60,60.9%也取60%

然而當總額=4000 剩余數量=1680時毫别,結果應該是正正好58%,
但程序運行時向下取整后結果卻是57%台丛,
我:砾肺??侠坎?
經過排查服務端取到的數據沒有問題硅蹦,上下文代碼也沒問題闷煤,那么問題出在哪兒呢鲤拿。


還不是因為我用了float類型

在說這個bug之前我們來復習一下幾個基礎知識

  • 帶小數位的十進制數如何轉為二進制數
  • float / double 類型是如何存儲的

關于第一點大部分程序員都已經會了署咽,簡單的說就是小數部分乘2取整,不會的同學請自行百度一下窒升,不占用篇幅饱须,重點是小數部分換算二進制時台谊,時常發(fā)生無限循環(huán),例如0.1酪呻,大家可以自己嘗試計算一下

關于float和double的存儲方式盐须,以float舉例(咱們挑和本篇問題有關的內容說

float類型即浮點型,一個float占4個字節(jié)
4個字節(jié)一共32位
從左往右第1位表示這個數的正負(a)
再往右的8個數位用于表示該浮點數存儲的指數部分(b)
最右邊的23位表示這個數字的有效數位(c)

正負表示位(1位) + 指數部分(8位) + 底數部分(23位) = 32位
abbbbbbb / bccccccc / cccccccc / cccccccc

然而23個底數位是不可能完整表示無限循環(huán)小數的


精度不夠闷尿,沒東西湊

一個無限循環(huán)眼溶,一個只有23位底數,那么結果是什么灌旧?
精度丟失绰筛,23位之后的數字無法表示铝噩,被當作0處理了
看回到我的bug上

當總額=4000 剩余數量=1680時,結果應該是正正好58%毛甲,
但程序運行時向下取整后結果卻是57%

經過計算

(4000 - 1680) / 4000 = 0.58
而0.58轉為2進制是無限循環(huán)的小數
float類型取了23個有效數位后具被,后面的數位給扔了……
實際上計算的結果為 0.5799999999.....
向下取整為 0.57
0.57 * 100% = 57%

原因就是如此,所以說啊七咧,基本功是很重要的艾栋,


那咋辦呢蛉顽?

1.最簡單的方法,不出現小數
在這個bug里遥诉,最簡單的修復方式是

(4000 - 1680) / 4000 * 100
改為
(4000 - 1680) * 100 / 4000

即將小數化為百分數的乘以100提前噪叙,使得小數不出現睁蕾,就不會存在無限循環(huán)數了(事實證明這樣確實有效)
但這只僅限于使用百分比的情況债朵,如果是一個大額度交易的精確計算序芦,需要用以下第2點的方法

2.有的時候出現小數是不可避免的粤咪,而同時需要非常高的精度,不容誤差(金錢宪塔、折扣方面的計算)囊拜;
那么這個時候,就要掏出我們針對float/double類型的計算工具了 —— NSDecimalNumber

NSDecimalNumber是基于十進制的定點計算冠跷,所以不會產生精度誤差

image.png

一個定點數包含了:用一個尾數(Mantissa)蜜托、一個基數(Base)、一個指數(Exponent)以及一個表示正負的符號(sign).
比如 15.99 用十進制科學計數法可以表達為 +1599 × 10?2 怠苔,其中 1.2345 為尾數仪糖,10 為基數迫肖,2 為指數。sign為 ‘+’故爵。

來源:NSDecimalNumber的介紹和使用

具體的使用請查閱文檔隅津,本文僅做一個提醒,歡迎批評指正探討


附帶一個JAVA的小函數结窘,用于將數字展開為double類型的存儲格式
(寫這篇博文的時候找不到這段代碼的出處了充蓝,看到出處的朋友請告訴我一下我附上鏈接)

public class showDouble {
  public static void main(String[] args) {
    printBits(3.54);

  }

  private static void printBits(double d) {
    System.out.println("##"+d);
    long l = Double.doubleToLongBits(d);
    String bits = Long.toBinaryString(l);
    int len = bits.length();
    System.out.println(bits+"#"+len);
    if(len == 64) {
        System.out.println("[63]"+bits.charAt(0));
        System.out.println("[62-52]"+bits.substring(1,12));
        System.out.println("[51-0]"+bits.substring(12, 64));
    } else {
        System.out.println("[63]0");
        System.out.println("[62-52]"+ pad(bits.substring(0, len - 52)));
        System.out.println("[51-0]"+bits.substring(len-52, len));
    }
  }

  private static String pad(String exp) {
    int len = exp.length();
    if(len == 11) {
        return exp;
    } else {
        StringBuilder sb = new StringBuilder();
        for (int i = 11-len; i > 0; i--) {
            sb.append("0");
        }
        sb.append(exp);
        return sb.toString();
    }
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末官脓,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子卑笨,更是在濱河造成了極大的恐慌,老刑警劉巖妖滔,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铛楣,死亡現場離奇詭異艺普,居然都是意外死亡,警方通過查閱死者的電腦和手機岸浑,發(fā)現死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門矢洲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缩焦,“玉大人,你說我怎么就攤上這事袁滥√夥” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵塑荒,是天一觀的道長姜挺。 經常有香客問我,道長偎窘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任他托,我火速辦了婚禮赏参,結果婚禮上,老公的妹妹穿的比我還像新娘把篓。我一直安慰自己腰涧,他們只是感情好,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布疗锐。 她就那樣靜靜地躺著滑臊,像睡著了一般箍铲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颠猴,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天,我揣著相機與錄音祭玉,去河邊找鬼春畔。 笑死岛都,一個胖子當著我的面吹牛,可吹牛的內容都是我干的择份。 我是一名探鬼主播荣赶,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拔创!你這毒婦竟也來了剩燥?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤侣滩,失蹤者是張志新(化名)和其女友劉穎变擒,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體策添,經...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡舰攒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年摩窃,在試婚紗的時候發(fā)現自己被綠了猾愿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片账阻。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡淘太,死狀恐怖,靈堂內的尸體忽然破棺而出蒲牧,到底是詐尸還是另有隱情,我是刑警寧澤松嘶,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布翠订,位于F島的核電站,受9級特大地震影響尽超,放射性物質發(fā)生泄漏橙弱。R本人自食惡果不足惜歧寺,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棘脐。 院中可真熱鬧斜筐,春花似錦、人聲如沸蛀缝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屈梁。三九已至嗤练,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間在讶,已是汗流浹背煞抬。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留构哺,地道東北人革答。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像曙强,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子溪食,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內容

  • 背景 在java中float賦值給double雀瓢,會產生精度問題。 輸出為2.0999999046325684排截。 小...
    我叫小小強閱讀 19,218評論 2 23
  • 前言 在日常的開發(fā)中我們隨時都會跟數字打著交道智政,對數字的處理也是很平常的事续捂,本文僅對常用的數字操作一個小結牙瓢,當一個...
    進無盡閱讀 1,098評論 0 2
  • 浮點類型 用于表示有小數部分的數值。Java中有兩種浮點類型胁附, 也可以用16進制表示浮點數值。例如0.125=2-...
    獅_子歌歌閱讀 1,266評論 0 2
  • floata =0.01;intb =99999999;doublec =0.0;c = a*b;NSLog(@"...
    Lv明閱讀 1,074評論 0 0
  • 目錄 1洗做、八種基本數據類型 八種數據類型分別是:(整數型) byte撰筷,short毕籽,int关筒,long蒸播,(浮點型)fl...
    AnchEvil閱讀 1,587評論 0 0