前言
在日常的開發(fā)中我們隨時都會跟數(shù)字打著交道,對數(shù)字的處理也是很平常的事,本文僅對常用的數(shù)字操作一個小結(jié)驻仅,當(dāng)一個筆記方便日后查看乏奥,也希望讀者能從中收獲些感覺有用的知識摆舟。
現(xiàn)實(shí)中使用數(shù)字場景下存在的誤差
對于數(shù)字要求比較嚴(yán)格的莫過于跟錢有關(guān)的 單價、總價等邓了,
亦或者 浮點(diǎn)數(shù)在總數(shù)中占有的百分比計(jì)算恨诱,這些都是對價格要求比較嚴(yán)格的,
而使用 floatValue doubleValue 的轉(zhuǎn)化計(jì)算骗炉,往往出現(xiàn)的誤差是讓人抓狂的
-
計(jì)算的時候
NSString *s = @"22.33"; [s floatValue] : 22.3299999 [s doubleValue] : 22.329999999999998
也許你說這有什么照宝,四舍五入不就好了,可是當(dāng)很多個被你四舍五入的數(shù)字進(jìn)行大量的運(yùn)算后句葵,最終的結(jié)果和實(shí)際的結(jié)果之間的差異還是讓人無法接受的厕鹃。
比較的時候
也許少量的計(jì)算在你使用你四舍五入的數(shù)字后最終的結(jié)果和實(shí)際的差不多,但是當(dāng)你進(jìn)行浮點(diǎn)型小數(shù)之間的比較時就炸了
if ([@"0.01" floatValue]<0.01)
沒錯這個比較返回的是 ture, 0.01<0.01笼呆,你瞬間無語了吧熊响,不相信,再次運(yùn)行诗赌,結(jié)果還是 ture汗茄。
為什么使用floatValue、doubleValue 轉(zhuǎn)化后的數(shù)據(jù)會出現(xiàn)誤差铭若。
要回答這點(diǎn)洪碳,我們先要明白這是浮點(diǎn)數(shù)在計(jì)算機(jī)中的存儲方式就決定的。先來了解下浮點(diǎn)數(shù)在計(jì)算機(jī)中的存儲方式叼屠。
我們都知道在計(jì)算機(jī)的內(nèi)存中瞳腌,任何數(shù)據(jù)都是以0、1的形式被存儲記錄的镜雨,每一個這樣的存儲單位叫做位(bit)嫂侍,這也是二進(jìn)制的實(shí)現(xiàn)基礎(chǔ)。
整數(shù)的存儲方式:
計(jì)算機(jī)用二進(jìn)制來表示整數(shù)荚坞,最高位是符號位挑宠;-
浮點(diǎn)數(shù)的存儲方式:
以intel的處理器為例,方便起見颓影,這里只以float型為例——從存儲結(jié)構(gòu)和算法上來講各淀,double和float是一樣的,不一樣的地方僅僅是float是32位的诡挂,double是64位的碎浇,所以double能存儲更高的精度临谱。首先了解如何用二進(jìn)制表示小數(shù)(也就是如何把十進(jìn)制小數(shù)轉(zhuǎn)化為二進(jìn)制表示)這一步很重要是你理解為什么出現(xiàn)誤差的關(guān)鍵。
舉一個簡單例子奴璃,十進(jìn)制小數(shù) 10.625
(1)首先轉(zhuǎn)換整數(shù)部分: 10 = 1010
(2)小數(shù)部分0.625 = 0.101
十進(jìn)制小數(shù)二進(jìn)制化:(用“乘2取整法”:
0.6252=1.25,得第一位為1悉默,
0.252=0.5, 得第二位為0溺健,
0.5*2=1麦牺, 得第三位為1,
余下小數(shù)部分為零鞭缭,就可以結(jié)束了)
(3)于是得到 10.625=1010.101
(4) 類似十進(jìn)制可以用指數(shù)形式表示: 10.625=1.0625*(10^1) 所得的二進(jìn)制小數(shù)也可以這樣指數(shù)形式表述: 1010.101=1.010101 * (2^3) 也就是用有效數(shù)字a和指數(shù)e來表述: a *(2^e)尾數(shù)部分就可以表示為xxxx,第一位都是1剖膳,可以將小數(shù)點(diǎn)前面的1省略,所以23bit的尾數(shù)部分岭辣,可以表示的精度卻變成了24bit吱晒,道理就是在這里,那24bit能精確到小數(shù)點(diǎn)后幾位呢沦童,我們知道9的二進(jìn)制表示為1001仑濒,所以4bit能精確十進(jìn)制中的1位小數(shù)點(diǎn),24bit(float)就能使float能精確到小數(shù)點(diǎn)后6位偷遗,而對于指數(shù)部分墩瞳,因?yàn)橹笖?shù)可正可負(fù),8位的指數(shù)位能表示的指數(shù)范圍就應(yīng)該為:-128-127氏豌。
至于想知道為什么是 -128-127而不是 -127-127的同學(xué)可以看這里 為什么8位的二進(jìn)制補(bǔ)碼范圍是-128-127喉酌,而不是-127-127 。
一個32bit的空間(bit0~bit31) | 表示的意義 |
---|---|
bit0~bit22 共23bit | 用來表示有效數(shù)字部分泵喘,也就是a泪电,本例中補(bǔ)全后面的0之后 a變?yōu)?10 1010 0000 0000 0000 0000 |
bit23~bit30 共8個bit | 用來表是指數(shù),也就是e纪铺,范圍從-128到127相速,實(shí)際數(shù)據(jù)中的指數(shù)是原始指數(shù)加上127得到的,如果超過了127鲜锚,則從-128開始計(jì)突诬,所以這里e=3表示為130 |
bit31 共1位 | 為符號位,1表示負(fù)數(shù) |
所以 8.25 在計(jì)算機(jī)的實(shí)際存儲中是這樣存儲的
其中float的存儲方式如下圖所示:
而 double 的存儲方式為:
注意這個例子的特殊性:它的小數(shù)部分正好可以用有限長度的2進(jìn)制小數(shù)表示芜繁,因此攒霹,而且整個有效數(shù)字部分a的總長度小于23,因此它精確的表示了10.625浆洗,但是有的情況下,有效數(shù)字部分的長度可能超過23集峦,甚至是無限多的伏社,那時候就只好把后面的位數(shù)截掉了抠刺,那樣表示的結(jié)果就只是一個近似值而非精確值;顯然摘昌,存儲長度越長速妖,精度就越高,比如雙精度浮點(diǎn)數(shù)長度為64位聪黎,1位符號位罕容,11位指數(shù)位,52位有效數(shù)字稿饰。
那些被裁掉丟失的數(shù)據(jù)就是造成浮點(diǎn)型數(shù)據(jù)保存后不精確的原因所在锦秒。
如何愉快與數(shù)字玩耍
-
酌情避免使用 float ,更多地使用 double
float類型的最大容量是8位(大于15萬的浮點(diǎn)數(shù)字就會出現(xiàn)不精確了(筆者做過遍歷測試)喉镰,而double類型的容量為16位(在數(shù)十億的范圍內(nèi)都是字面上精確的旅择。),所以在項(xiàng)目開發(fā)過程中字符串和浮點(diǎn)類型的轉(zhuǎn)換最好用double類型侣姆。但是double類型如果超出16位也會失真生真。#通過和NSString的轉(zhuǎn)換娄猫,將計(jì)算的原始數(shù)據(jù)轉(zhuǎn)換為純粹的double類型的數(shù)據(jù)幅恋, #這樣的計(jì)算精度就可以達(dá)到要求了** NSString *objA = [NSString stringWithFormat:@"%.2f", a]; NSString *objB = [NSString stringWithFormat:@"%.2f", (double)b]; c = [objA doubleValue] * [objB doubleValue]; NSLog(@"%.2f",c); //輸出結(jié)果 999999.99
- 如果涉及到精密計(jì)算的問題烫罩,可以轉(zhuǎn)化為NSDecimalNumber對象來操作瞄桨。
NSDecimalNumber--十進(jìn)制數(shù)
iOS提供的一種支持準(zhǔn)確精度計(jì)算的數(shù)據(jù)類型 NSDecimalNumber. NSDecimalNumber是NSNumber的子類录语,比NSNumber的功能更為強(qiáng)大敲长,它們被設(shè)計(jì)為執(zhí)行基礎(chǔ)10計(jì)算技俐,而不會損失精度并具有可預(yù)測的舍入行為床估⊥淠遥可以指定一個數(shù)的冪痰哨,四舍五入等操作。由于NSDecimalNumber精度較高匾嘱,所以會比基本數(shù)據(jù)類型費(fèi)時斤斧,所以需要權(quán)衡考慮,
不過蘋果官方建議在貨幣以及要求精度很高的場景下使用霎烙。
NSDecimalNumber 創(chuàng)建對象(常用的方法)
+ (NSDecimalNumber *)decimalNumberWithMantissa:(unsigned long long)mantissa exponent:(short)exponent isNegative:(BOOL)flag;
mantissa:長整形撬讽;exponent:指數(shù);flag:正負(fù)數(shù)悬垃。
NSDecimalNumber *subtotalAmount = [NSDecimalNumber decimalNumberWithMantissa:
1275 exponent:-2 isNegative:NO]; //12.75
subtotalAmount = [NSDecimalNumber decimalNumberWithMantissa:
1275 exponent:2 isNegative:YES]; //-127500
+ (NSDecimalNumber *)decimalNumberWithString:(nullable NSString *)numberValue;
將字符串轉(zhuǎn)成一個十進(jìn)制數(shù)游昼。
NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithString:@"-12.74"]; //-12.74
discountAmount = [NSDecimalNumber decimalNumberWithString:@"127.4"]; //127.4
+ (NSDecimalNumber *)decimalNumberWithString:(nullable NSString *)numberValue locale:(nullable id)locale;
這個有點(diǎn)復(fù)雜,locale代表一種格式尝蠕,就像date的格式化一樣烘豌。這里的locale可以傳遞兩種格式
NSDictionary類型:
NSDictionary *locale = [NSDictionary dictionaryWithObject:@"," forKey:NSLocaleDecimalSeparator]; //以","當(dāng)做小數(shù)點(diǎn)格式
NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithString:@"123,40" locale:locale]; //123.4
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"fr_FR"]; //法國數(shù)據(jù)格式,法國的小數(shù)點(diǎn)是','逗號
NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithString:@"123,40" locale:locale]; //123.4
其他常用方法
+(NSDecimalNumber *)zero; //0
+(NSDecimalNumber *)one; //1
+(NSDecimalNumber *)minimumDecimalNumber;
//-3402823669209384634633746074317682114550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+(NSDecimalNumber *)maximumDecimalNumber;
//3402823669209384634633746074317682114550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+(NSDecimalNumber *)notANumber;
//非數(shù)字,常用于對比看彼,比如:
[[NSDecimalNumber notANumber] isEqualToNumber:myNumber];
NSDecimalNumber 邏輯運(yùn)算
-
加法運(yùn)算
-(NSDecimalNumber *)decimalNumberByAdding:(NSDecimalNumber *)decimalNumber; -(NSDecimalNumber *)decimalNumberByAdding:(NSDecimalNumber *)decimalNumber withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
減法運(yùn)算
-(NSDecimalNumber *)decimalNumberBySubtracting:(NSDecimalNumber *)decimalNumber; -(NSDecimalNumber *)decimalNumberBySubtracting:(NSDecimalNumber *)decimalNumber withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
乘法運(yùn)算
-(NSDecimalNumber *)decimalNumberByMultiplyingBy:(NSDecimalNumber *)decimalNumber; -(NSDecimalNumber *)decimalNumberByMultiplyingBy:(NSDecimalNumber *)decimalNumber withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
除法運(yùn)算
-(NSDecimalNumber *)decimalNumberByDividingBy:(NSDecimalNumber *)decimalNumber; -(NSDecimalNumber *)decimalNumberByDividingBy:(NSDecimalNumber *)decimalNumber withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
a的n次方
-(NSDecimalNumber *)decimalNumberByRaisingToPower:(NSUInteger)power; -(NSDecimalNumber *)decimalNumberByRaisingToPower:(NSUInteger)power withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
指數(shù)運(yùn)算
-(NSDecimalNumber *)decimalNumberByMultiplyingByPowerOf10:(short)power; -(NSDecimalNumber *)decimalNumberByMultiplyingByPowerOf10:(short)power withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
比較運(yùn)算
-(NSComparisonResult)compare:(NSNumber *)decimalNumber;
使用式例
NSDecimalNumber *discount1 = [NSDecimalNumber decimalNumberWithString:@"1.2"]; NSDecimalNumber *discount2 = [NSDecimalNumber decimalNumberWithString:@"1.3"]; NSComparisonResult result = [discount1 compare:discount2]; if (result == NSOrderedAscending) { # 升序 后者比前者大 NSLog(@"1.2 < 1.3"); } else if (result == NSOrderedSame) { NSLog(@"1.2 == 1.3"); } else if (result == NSOrderedDescending) { # 降序 后者比前者小 NSLog(@"1.2 > 1.3"); }
NSDecimalNumberBehaviors 是 邏輯運(yùn)算中帶的行為
NSDecimalNumberBehaviors對象可以通過下述方法創(chuàng)建
NSDecimalNumberHandler *roundUp = [NSDecimalNumberHandler
decimalNumberHandlerWithRoundingMode:NSRoundBankers
scale:2
raiseOnExactness:NO
raiseOnOverflow:NO
raiseOnUnderflow:NO
raiseOnDivideByZero:YES];
參數(shù) | 含義 |
---|---|
roundingMode | 四舍五入模式廊佩,有四個值: NSRoundUp, NSRoundDown, NSRoundPlain, and NSRoundBankers |
scale | 結(jié)果保留幾位小數(shù) |
raiseOnExactness | 發(fā)生精確錯誤時是否拋出異常囚聚,一般為NO |
raiseOnOverflow | 發(fā)生溢出錯誤時是否拋出異常,一般為NO |
raiseOnUnderflow | 發(fā)生不足錯誤時是否拋出異常标锄,一般為NO |
raiseOnDivideByZero | 被0除時是否拋出異常顽铸,一般為YES |
#枚舉:
NSRoundPlain, // Round up on a tie //四舍五入
NSRoundDown, // Always down == truncate //只舍不入
NSRoundUp, // Always up // 只入不舍
NSRoundBankers 四舍六入, 中間值時, 取最近的,保持保留最后一位為偶數(shù)
參照一下圖片, 理解上面枚舉值:
當(dāng)他們試圖除以0或產(chǎn)生一個數(shù)表示太大或太小的時候發(fā)生異常。
下面列出了各種異常的名字 表明NSDecimalNumber計(jì)算錯誤料皇。
extern NSString *NSDecimalNumberExactnessException; //如果出現(xiàn)一個精確的錯誤
extern NSString *NSDecimalNumberOverflowException; // 溢出
extern NSString *NSDecimalNumberUnderflowException; //下溢
extern NSString *NSDecimalNumberDivideByZeroException; //除數(shù)為0
NSDecimalNumber *sub = [[NSDecimalNumber alloc]initWithFloat:1.23];
sub = [sub decimalNumberByAdding:sub
withBehavior:[NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundDown
scale:1 raiseOnExactness:NO raiseOnOverflow:NO raiseOnUnderflow:NO raiseOnDivideByZero:YES]];
# 這里特別提醒一下:RoundingMode 中 NSRoundDown模式下的 NSDecimalNumber數(shù)值 floatValue谓松、doubleValue 后依然會出現(xiàn)不精確的問題。
# 其他模式下倒沒有這樣的現(xiàn)象践剂。
.
..
大量使用NSDecimalNumber需要注意的問題
大量NSDecimalNumber 進(jìn)行計(jì)算時比較消耗系統(tǒng)性能鬼譬,必要時可以使用 C語言級別的NSDecimal 來代替運(yùn)算,這可以減少不少的系統(tǒng)開銷舷手。NSDecimal是C語言級別的無法直接創(chuàng)建拧簸,不幸的是,基礎(chǔ)框架沒有直接創(chuàng)建的方法男窟,你只能先創(chuàng)建生成一個 NSDecimalNumber 再得到對應(yīng)的 NSDecimal盆赤。
# NSDecimal 與 NSDecimalNumber 之間的轉(zhuǎn)化
NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithString:@"15.99"];
NSDecimal asStruct = [price decimalValue];
NSDecimalNumber *asNewObject = [NSDecimalNumber decimalNumberWithDecimal:asStruct];
NSDecimal的使用中需要注意
C接口使用類似的功能NSDecimalAdd(), NSDecimalSubtract()不是返回結(jié)果歉眷,這些函數(shù)用計(jì)算的值填充第一個參數(shù)牺六。
這使得可以重用現(xiàn)有NSDecimal的幾個操作,并避免分配不必要的結(jié)構(gòu)只是為了保存中間值汗捡。
NSDecimal price1 = [[NSDecimalNumber decimalNumberWithString:@"15.99"] decimalValue];
NSDecimal price2 = [[NSDecimalNumber decimalNumberWithString:@"29.99"] decimalValue];
NSDecimal coupon = [[NSDecimalNumber decimalNumberWithString:@"5.00"] decimalValue];
NSDecimal discount = [[NSDecimalNumber decimalNumberWithString:@".90"] decimalValue];
NSDecimal numProducts = [[NSDecimalNumber decimalNumberWithString:@"2.0"] decimalValue];
NSLocale *locale = [NSLocale currentLocale];
NSDecimal result;
NSDecimalAdd(&result, &price1, &price2, NSRoundUp);
NSLog(@"Subtotal: %@", NSDecimalString(&result, locale));
NSDecimalSubtract(&result, &result, &coupon, NSRoundUp);
NSLog(@"After coupon: %@", NSDecimalString(&result, locale));
NSDecimalMultiply(&result, &result, &discount, NSRoundUp);
NSLog(@"After discount: %@", NSDecimalString(&result, locale));
NSDecimalDivide(&result, &result, &numProducts, NSRoundUp);
NSLog(@"Average price per product: %@", NSDecimalString(&result, locale));
NSDecimalPower(&result, &result, 2, NSRoundUp);
NSLog(@"Average price squared: %@", NSDecimalString(&result, locale));
其他常用數(shù)字處理方法
.
# 浮點(diǎn)型小數(shù)四舍五入 afterPoint: 小數(shù)點(diǎn)后幾位
+(NSString *)notRounding:(float)price afterPoint:(int)position{
NSDecimalNumberHandler* roundingBehavior = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundPlain scale:position raiseOnExactness:NO raiseOnOverflow:NO raiseOnUnderflow:NO raiseOnDivideByZero:NO];
NSDecimalNumber *ouncesDecimal;
NSDecimalNumber *roundedOunces;
ouncesDecimal = [[NSDecimalNumber alloc] initWithFloat:price];
roundedOunces = [ouncesDecimal decimalNumberByRoundingAccordingToBehavior:roundingBehavior];
return [NSString stringWithFormat:@"%@",roundedOunces];
}
# 浮點(diǎn)數(shù)處理并去掉多余的0
- (NSString *)stringDisposeWithFloat:(double)floatValue
{
NSString *str = [NSString stringWithFormat:@"%f",floatValue];
NSInteger len = str.length;
for (NSInteger i = 0; i < len; i++)
{
if (![str hasSuffix:@"0"])
break;
else
str = [str substringToIndex:[str length]-1];
}
if ([str hasSuffix:@"."])//避免像2.0000這樣的被解析成2. 以淑际。。扇住。結(jié)尾
{
return [str substringToIndex:[str length]-1];//s.substring(0, len - i - 1);
}
else
{
return str;
}
}
# 數(shù)字3位加一個逗號
+(NSString *)countNumAndChangeformat:(NSString *)num
{
int count = 0;
long long int a = num.longLongValue;
while (a != 0)
{
count++;
a /= 10;
}
NSMutableString *string = [NSMutableString stringWithString:num];
NSMutableString *newstring = [NSMutableString string];
while (count > 3) {
count -= 3;
NSRange rang = NSMakeRange(string.length - 3, 3);
NSString *str = [string substringWithRange:rang];
[newstring insertString:str atIndex:0];
[newstring insertString:@"," atIndex:0];
[string deleteCharactersInRange:rang];
}
[newstring insertString:string atIndex:0];
return newstring;
}
小結(jié)
數(shù)字的處理是及其常見的春缕,本文到此就結(jié)束了,后續(xù)如有新的歸納會及時更新上來艘蹋,希望看完這篇文章的朋友能有所收獲锄贼。文中如有錯誤,歡迎留言指正女阀。
參考文章:
‘NSDecimalNumber--十進(jìn)制數(shù)’使用方法
NSDecimalNumber
iOS 中的數(shù)據(jù)結(jié)構(gòu)和算法(一):浮點(diǎn)數(shù)
存儲方式