起源
前幾天改了同事遺留的一個(gè)四舍五入的缺陷,頗有探索的價(jià)值。問(wèn)題簡(jiǎn)化如下:
總邀約人數(shù)11人,已完成6人,邀約完成率應(yīng)顯示為55%,實(shí)際顯示54%
廢話不多說(shuō)翻代碼:
C#:
int CalcPercentageInt(int a, int b)
{
if (b == 0 || a == 0) return 0;
int result = 100 * a / b;
return result;
}
簡(jiǎn)短的一句代碼有諸多想法:
1.四舍五入的代碼應(yīng)該放到前端去做,能稍微減輕服務(wù)器壓力。
- int做輸入?yún)?shù),要考慮精度損失問(wèn)題。
3.輸出參數(shù)為什么是int, 又要考慮精度問(wèn)題,直接輸出格式化的好的百分比不就行了
后面又有同事改造成如下:
int CalcPercentageInt(int a, int b)
{
if (b == 0 || a == 0) return 0;
string result = ((double)a / b).ToString("f2"); ;
return (int)(Convert.ToDouble(result) * 100);
}
執(zhí)行以上運(yùn)算結(jié)果是55,是對(duì)的。然后他就提測(cè)了,結(jié)果被打回來(lái)了。
總邀約人數(shù)7人,已完成2人,邀約完成率應(yīng)顯示為29%,實(shí)際顯示28%
然后缺陷轉(zhuǎn)到我名下了,我改為如下版本:
int CalcPercentageIntBak(double a, double b)
{
if (b == 0 || a == 0) return 0;
var result = Math.Round(a/b,2)*100 ;// 這一步 2/7 得到28.999999999999996
return (int)result;
}
這樣也不行,改為decimal可以了
int CalcPercentageInt(decimal a, decimal b)
{
if (b == 0 || a == 0) return 0;
var result = Math.Round(a/b,2)*100 ;// 這一步 2/7 得到29.00M
return (int)result;
}
經(jīng)單元測(cè)試如下都通過(guò):
int m1 = CalcPercentageInt(2, 7);
Assert.True(m1 == 29);
... 3到5省略
m1 = CalcPercentageInt(6, 7);
Assert.True(m1 == 86);
m1 = CalcPercentageInt(2, 9);
... 3到7省略
m1 = CalcPercentageInt(8, 9);
Assert.True(m1 == 89);
m1 = CalcPercentageInt(2, 11);
Assert.True(m1 == 18);
... 3到9省略
m1 = CalcPercentageInt(10, 11);
Assert.True(m1 == 91);
雖然解決了問(wèn)題,最合理的方案也許還是放到前端去計(jì)算,或者直接toString("f2")都比返回int到前端好些。用chrome演示如下(多么的省事!)
>((2/7)*100).toFixed('0')
"29"
>((3/7)*100).toFixed('0')
"43"
>((6/11)*100).toFixed('0')
"55"
再來(lái)對(duì)比下如下寫法:
//javascript
> var a=6,b=11;
parseInt(a)/parseInt(b)
輸出:0.5454545454545454
> var a=2,b=7;
parseInt(a)/parseInt(b)
輸出: 0.2857142857142857
//C#
var a=2;
var b=11;
var m1=a/b; 輸出:0
var m2=(double)a/b;輸出:0.2857142857142857
var m3=(decimal)a/b;輸出:0.2857142857142857142857142857
a=2;b=7;
(float)2/7;輸出:0.2857143
(double)2/7;輸出:0.2857142857142857
(decimal)2/7;輸出:0.2857142857142857142857142857
Math.Round((double)a/b,2) 輸出:0.29
Math.Round((double)a/b,2)*100,輸出:28.999999999999996 (上一步是0.29 ,*100后變成了28.999999999999996,佛系吧)
Math.Round((decimal)2/7,2),輸出:0.29M
Math.Round((decimal)2/7,2)*100,輸出29.00(可以對(duì)比double,為什么?)
從上面的對(duì)比可以知道int型除法小數(shù)精度問(wèn)題C#和javascript采取的策略是不一樣的,以及C#中double和decimal的處理方式也有所不同。
我們?cè)賮?lái)看下C++:
//gcc version 9.2.0 (MinGW.org GCC Build-2)(2019年8月5號(hào)發(fā)布)
int main()
{
int a = 2;
int b = 7;
cout << "a/b的結(jié)果是:" << a / b << endl;
//輸出:0
cout << "(double)a/b 的結(jié)果是:" << (double)a / b << endl;
//輸出:0.285714(注意這個(gè)小數(shù)位長(zhǎng)度只有6個(gè))
//cout << "(double)a / b" << (decimal)a / b;//C++默認(rèn)沒(méi)有decimal類型?
}
針對(duì)這些亂象,我們要從一個(gè)浮點(diǎn)數(shù)的標(biāo)準(zhǔn)IEEE754說(shuō)起。
IEEE754
IEEE二進(jìn)制浮點(diǎn)數(shù)算術(shù)標(biāo)準(zhǔn)(IEEE 754)是20世紀(jì)80年代以來(lái)最廣泛使用的浮點(diǎn)數(shù)運(yùn)算標(biāo)準(zhǔn),為許多CPU與浮點(diǎn)運(yùn)算器所采用。這個(gè)標(biāo)準(zhǔn)定義了表示浮點(diǎn)數(shù)的格式(包括負(fù)零-0)與反常值(denormal number)),一些特殊數(shù)值(無(wú)窮(Inf)與非數(shù)值(NaN)),以及這些數(shù)值的“浮點(diǎn)數(shù)運(yùn)算符”;它也指明了四種數(shù)值舍入規(guī)則和五種例外狀況(包括例外發(fā)生的時(shí)機(jī)與處理方式)。
Ieee754-2019官方鏈接
下載IEEE-754-2019
該標(biāo)準(zhǔn)規(guī)定了計(jì)算機(jī)編程環(huán)境中二進(jìn)制和十進(jìn)制浮點(diǎn)算術(shù)的交換和算術(shù)格式以及方法。該標(biāo)準(zhǔn)規(guī)定了異常條件及其默認(rèn)處理??梢酝耆攒浖?,完全以硬件或以軟件和硬件的任何組合來(lái)實(shí)現(xiàn)符合該標(biāo)準(zhǔn)的浮點(diǎn)系統(tǒng)的實(shí)現(xiàn)。對(duì)于本標(biāo)準(zhǔn)規(guī)范部分中指定的操作,數(shù)值結(jié)果和例外情況由輸入數(shù)據(jù)的值,操作順序和目標(biāo)格式唯一確定,所有這些操作均在用戶的控制之下。
相關(guān)標(biāo)準(zhǔn)的其他版本(包含已被取代)有:
IEEE 754-1985-二進(jìn)制浮點(diǎn)算法的IEEE標(biāo)準(zhǔn)
IEEE 854-1987-獨(dú)立于基數(shù)的浮點(diǎn)算法的IEEE標(biāo)準(zhǔn)
IEEE 754-2008-浮點(diǎn)算法的IEEE標(biāo)準(zhǔn)
IEEE / ISO / IEC 60559-2020-ISO / IEC / IEEE國(guó)際標(biāo)準(zhǔn)-浮點(diǎn)運(yùn)算
這里有一篇文章IEEE 754格式可以作為參考,解答一下疑惑。
問(wèn)題
選擇一個(gè)標(biāo)準(zhǔn)方法用二進(jìn)制數(shù)來(lái)表示浮點(diǎn)數(shù)時(shí),需要考慮很多事情:
- 范圍:應(yīng)該能支持很大范圍的正負(fù)數(shù)
- 精度:你能區(qū)別1.7和1.8之間的區(qū)別么?1.700001和1.700002呢,你應(yīng)該記住多少小數(shù)位?
- 時(shí)間效率:您的解決方案是否使快速進(jìn)行比較和算術(shù)運(yùn)算變得容易?
- 空間注意:怎么極精確表示3的平方根,除非需要兆字節(jié)來(lái)存儲(chǔ)它
- 一對(duì)一的關(guān)系:如果每個(gè)浮點(diǎn)數(shù)只能以一種方式寫入,反之亦然,您的解決方案將簡(jiǎn)單得多
IEEE 754 Form的開發(fā)人員最終采用的方法使用科學(xué)符號(hào)的思想??茖W(xué)記數(shù)法是表達(dá)數(shù)字的標(biāo)準(zhǔn)方法,它使數(shù)字易于閱讀和比較。我們最熟悉的是以10為底的數(shù)字的科學(xué)計(jì)數(shù)法。
您只需要將數(shù)字分為兩部分:值的范圍1<=N<10,冪為10,例如:
3498523 被寫成
? 0.0432 被寫成
用二進(jìn)制數(shù)也是相似的思路,需要使用2的冪。只需將您的數(shù)字分解為大小在范圍內(nèi)的值1 ≤ ? < 2,并且為2的冪。
-6.84 被寫成
0.05 被寫成
要?jiǎng)?chuàng)建位串(二進(jìn)制串?),我們需要用下面的格式:
我們可以從中獲得三個(gè)關(guān)鍵信息:
- 第一部分:sign/符號(hào),如果符號(hào)位為0,則代表正數(shù),
=1; 如果符號(hào)位為1,代表負(fù)數(shù),$(-1)^0=-1; 否則為0;
- 第二部分:fraction/mantissa尾數(shù)我們總是把括號(hào)里的數(shù)字算作(1+某個(gè)分?jǐn)?shù))。因?yàn)槲覀冎?在那里,唯一重要的是分?jǐn)?shù),我們將把它寫成二進(jìn)制字符串。
如果我們需要將二進(jìn)制值轉(zhuǎn)換回以10為基數(shù)的值,我們只需將每個(gè)數(shù)字乘以其位值,如以下示例所示:
=0.5
=0.25
=0.625
- 第三部分指數(shù)/階 上一步獲得的2的冪只是一個(gè)整數(shù)。注意,該整數(shù)可以是正數(shù)或負(fù)數(shù),分別取決于原始值是大還是小。我們需要存儲(chǔ)該指數(shù)-但是,使用兩者的補(bǔ)碼(帶符號(hào)的值的常用表示形式)會(huì)使這些值的比較更加困難。這樣,我們將一個(gè)稱為bias(偏差)的常數(shù)添加到指數(shù)中。通過(guò)在存儲(chǔ)指數(shù)之前對(duì)其進(jìn)行偏置,我們將其置于更適合比較的無(wú)符號(hào)范圍內(nèi)。
對(duì)于單精度浮點(diǎn),將-127到+ 127范圍內(nèi)的指數(shù)加上127以得到1到254范圍內(nèi)的值(0和255具有特殊含義),從而對(duì)指數(shù)產(chǎn)生偏倚。
對(duì)于雙精度,將1022到+1023范圍內(nèi)的指數(shù)加1023來(lái)獲得1到2046范圍內(nèi)的值(0和2047具有特殊含義),從而對(duì)其產(chǎn)生偏差。
偏差和2的冪的和是實(shí)際上進(jìn)入IEEE 754字符串的指數(shù)。請(qǐng)記住,指數(shù)=冪+偏差。(或者,冪=指數(shù)偏差)。該指數(shù)本身必須最終以二進(jìn)制形式表示-但考慮到加上偏差后我們有一個(gè)正整數(shù),則現(xiàn)在可以按常規(guī)方式完成此操作。
計(jì)算這些二進(jìn)制值后,可以將它們放入32位或64位字段中。這些數(shù)字的排列方式如下:
通過(guò)以這種方式排列字段,以使符號(hào)位位于最高有效位位置,偏斜指數(shù)位于中間,然后尾數(shù)位于最低有效位-結(jié)果值實(shí)際上將正確排序以進(jìn)行比較,無(wú)論是否它被解釋為浮點(diǎn)數(shù)或整數(shù)值。這樣可以使用定點(diǎn)硬件對(duì)浮點(diǎn)數(shù)進(jìn)行高速比較。
有一些特殊情況:
零
符號(hào)位= 0; 有偏指數(shù)(階碼)=全部0位; 分?jǐn)?shù)=全部0 位;-0和+0是不同的值,盡管它們相等正負(fù)無(wú)窮大(不知是否理解對(duì)?)
符號(hào)位=0 ,有偏指數(shù)(階碼)=全部1個(gè)位, 分?jǐn)?shù)=全部0 位,表示正無(wú)窮大;
符號(hào)位=1,有偏指數(shù)(階碼)=全部1個(gè)位,分?jǐn)?shù)=全部0 位,為負(fù)無(wú)窮大;-
NaN(非數(shù)字)
值NAN用于表示錯(cuò)誤值。當(dāng)指數(shù)字段為全零且?guī)Я惴?hào)位或尾數(shù)不是1后跟零的尾數(shù)時(shí),將表示此值。這是一個(gè)特殊值,可用于表示尚不包含值的變量。
image.png
image.png
image.png
image.png
Float,Double ,Decimal 有何區(qū)別?
Decimal,Double和Float變量類型在存儲(chǔ)值方面有所不同。精度是主要區(qū)別,其中float是單精度(32位)浮點(diǎn)數(shù)據(jù)類型,double是雙精度(64位)浮點(diǎn)數(shù)據(jù)類型,而Decimals(十進(jìn)制)是128位浮點(diǎn)數(shù)據(jù)類型。
float/single -32位(7位數(shù)字)
double-64位(15-16位)
decimal-128位(28-29位有效數(shù)字)
主要區(qū)別在于Floats和Doubles是二進(jìn)制浮點(diǎn)類型,而Decimal將值存儲(chǔ)為浮點(diǎn)小數(shù)點(diǎn)類型。因此,Decimal位數(shù)具有更高的精度,通常用于要求高度準(zhǔn)確性的貨幣(金融)應(yīng)用程序中。但是在性能方面,Decimal比雙精度和浮點(diǎn)型慢。
Decimal可以100%準(zhǔn)確地表示十進(jìn)制格式精度范圍內(nèi)的任何數(shù)字,而Float和Double不能準(zhǔn)確表示所有數(shù)字,即使數(shù)字在其各自格式精度范圍內(nèi)。
將IEEE 754浮點(diǎn)轉(zhuǎn)換為二進(jìn)制
示例:轉(zhuǎn)換為浮點(diǎn)型
Floating Point Representation
ieee-standard-754-floating-point-numbers
Decimal vs Double vs Float



