前言
在日常的開發中我們隨時都會跟數字打著交道,對數字的處理也是很平常的事,本文僅對常用的數字操作一個小結,當一個筆記方便日后查看,也希望讀者能從中收獲些感覺有用的知識。
現實中使用數字場景下存在的誤差
對于數字要求比較嚴格的莫過于跟錢有關的 單價、總價等,
亦或者 浮點數在總數中占有的百分比計算,這些都是對價格要求比較嚴格的,
而使用 floatValue doubleValue 的轉化計算,往往出現的誤差是讓人抓狂的
-
計算的時候
NSString *s = @"22.33"; [s floatValue] : 22.3299999 [s doubleValue] : 22.329999999999998
也許你說這有什么,四舍五入不就好了,可是當很多個被你四舍五入的數字進行大量的運算后,最終的結果和實際的結果之間的差異還是讓人無法接受的。
比較的時候
也許少量的計算在你使用你四舍五入的數字后最終的結果和實際的差不多,但是當你進行浮點型小數之間的比較時就炸了
if ([@"0.01" floatValue]<0.01)
沒錯這個比較返回的是 ture, 0.01<0.01,你瞬間無語了吧,不相信,再次運行,結果還是 ture。
為什么使用floatValue、doubleValue 轉化后的數據會出現誤差。
要回答這點,我們先要明白這是浮點數在計算機中的存儲方式就決定的。先來了解下浮點數在計算機中的存儲方式。
我們都知道在計算機的內存中,任何數據都是以0、1的形式被存儲記錄的,每一個這樣的存儲單位叫做位(bit),這也是二進制的實現基礎。
整數的存儲方式:
計算機用二進制來表示整數,最高位是符號位;-
浮點數的存儲方式:
以intel的處理器為例,方便起見,這里只以float型為例——從存儲結構和算法上來講,double和float是一樣的,不一樣的地方僅僅是float是32位的,double是64位的,所以double能存儲更高的精度。首先了解如何用二進制表示小數(也就是如何把十進制小數轉化為二進制表示)這一步很重要是你理解為什么出現誤差的關鍵。
舉一個簡單例子,十進制小數 10.625
(1)首先轉換整數部分: 10 = 1010
(2)小數部分0.625 = 0.101
十進制小數二進制化:(用“乘2取整法”:
0.6252=1.25,得第一位為1,
0.252=0.5, 得第二位為0,
0.5*2=1, 得第三位為1,
余下小數部分為零,就可以結束了)
(3)于是得到 10.625=1010.101
(4) 類似十進制可以用指數形式表示: 10.625=1.0625*(10^1) 所得的二進制小數也可以這樣指數形式表述: 1010.101=1.010101 * (2^3) 也就是用有效數字a和指數e來表述: a *(2^e)尾數部分就可以表示為xxxx,第一位都是1,可以將小數點前面的1省略,所以23bit的尾數部分,可以表示的精度卻變成了24bit,道理就是在這里,那24bit能精確到小數點后幾位呢,我們知道9的二進制表示為1001,所以4bit能精確十進制中的1位小數點,24bit(float)就能使float能精確到小數點后6位,而對于指數部分,因為指數可正可負,8位的指數位能表示的指數范圍就應該為:-128-127。
至于想知道為什么是 -128-127而不是 -127-127的同學可以看這里 為什么8位的二進制補碼范圍是-128-127,而不是-127-127 。
一個32bit的空間(bit0~bit31) | 表示的意義 |
---|---|
bit0~bit22 共23bit | 用來表示有效數字部分,也就是a,本例中補全后面的0之后 a變為010 1010 0000 0000 0000 0000 |
bit23~bit30 共8個bit | 用來表是指數,也就是e,范圍從-128到127,實際數據中的指數是原始指數加上127得到的,如果超過了127,則從-128開始計,所以這里e=3表示為130 |
bit31 共1位 | 為符號位,1表示負數 |
所以 8.25 在計算機的實際存儲中是這樣存儲的
其中float的存儲方式如下圖所示:
而 double 的存儲方式為:
注意這個例子的特殊性:它的小數部分正好可以用有限長度的2進制小數表示,因此,而且整個有效數字部分a的總長度小于23,因此它精確的表示了10.625,但是有的情況下,有效數字部分的長度可能超過23,甚至是無限多的,那時候就只好把后面的位數截掉了,那樣表示的結果就只是一個近似值而非精確值;顯然,存儲長度越長,精度就越高,比如雙精度浮點數長度為64位,1位符號位,11位指數位,52位有效數字。
那些被裁掉丟失的數據就是造成浮點型數據保存后不精確的原因所在。
如何愉快與數字玩耍
-
酌情避免使用 float ,更多地使用 double
float類型的最大容量是8位(大于15萬的浮點數字就會出現不精確了(筆者做過遍歷測試),而double類型的容量為16位(在數十億的范圍內都是字面上精確的。),所以在項目開發過程中字符串和浮點類型的轉換最好用double類型。但是double類型如果超出16位也會失真。#通過和NSString的轉換,將計算的原始數據轉換為純粹的double類型的數據, #這樣的計算精度就可以達到要求了** NSString *objA = [NSString stringWithFormat:@"%.2f", a]; NSString *objB = [NSString stringWithFormat:@"%.2f", (double)b]; c = [objA doubleValue] * [objB doubleValue]; NSLog(@"%.2f",c); //輸出結果 999999.99
- 如果涉及到精密計算的問題,可以轉化為NSDecimalNumber對象來操作。
NSDecimalNumber--十進制數
iOS提供的一種支持準確精度計算的數據類型 NSDecimalNumber. NSDecimalNumber是NSNumber的子類,比NSNumber的功能更為強大,它們被設計為執行基礎10計算,而不會損失精度并具有可預測的舍入行為??梢灾付ㄒ粋€數的冪,四舍五入等操作。由于NSDecimalNumber精度較高,所以會比基本數據類型費時,所以需要權衡考慮,
不過蘋果官方建議在貨幣以及要求精度很高的場景下使用。
NSDecimalNumber 創建對象(常用的方法)
+ (NSDecimalNumber *)decimalNumberWithMantissa:(unsigned long long)mantissa exponent:(short)exponent isNegative:(BOOL)flag;
mantissa:長整形;exponent:指數;flag:正負數。
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;
將字符串轉成一個十進制數。
NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithString:@"-12.74"]; //-12.74
discountAmount = [NSDecimalNumber decimalNumberWithString:@"127.4"]; //127.4
+ (NSDecimalNumber *)decimalNumberWithString:(nullable NSString *)numberValue locale:(nullable id)locale;
這個有點復雜,locale代表一種格式,就像date的格式化一樣。這里的locale可以傳遞兩種格式
NSDictionary類型:
NSDictionary *locale = [NSDictionary dictionaryWithObject:@"," forKey:NSLocaleDecimalSeparator]; //以","當做小數點格式
NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithString:@"123,40" locale:locale]; //123.4
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"fr_FR"]; //法國數據格式,法國的小數點是','逗號
NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithString:@"123,40" locale:locale]; //123.4
其他常用方法
+(NSDecimalNumber *)zero; //0
+(NSDecimalNumber *)one; //1
+(NSDecimalNumber *)minimumDecimalNumber;
//-3402823669209384634633746074317682114550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+(NSDecimalNumber *)maximumDecimalNumber;
//3402823669209384634633746074317682114550000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+(NSDecimalNumber *)notANumber;
//非數字,常用于對比,比如:
[[NSDecimalNumber notANumber] isEqualToNumber:myNumber];
NSDecimalNumber 邏輯運算
-
加法運算
-(NSDecimalNumber *)decimalNumberByAdding:(NSDecimalNumber *)decimalNumber; -(NSDecimalNumber *)decimalNumberByAdding:(NSDecimalNumber *)decimalNumber withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
減法運算
-(NSDecimalNumber *)decimalNumberBySubtracting:(NSDecimalNumber *)decimalNumber; -(NSDecimalNumber *)decimalNumberBySubtracting:(NSDecimalNumber *)decimalNumber withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
乘法運算
-(NSDecimalNumber *)decimalNumberByMultiplyingBy:(NSDecimalNumber *)decimalNumber; -(NSDecimalNumber *)decimalNumberByMultiplyingBy:(NSDecimalNumber *)decimalNumber withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
除法運算
-(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;
-
指數運算
-(NSDecimalNumber *)decimalNumberByMultiplyingByPowerOf10:(short)power; -(NSDecimalNumber *)decimalNumberByMultiplyingByPowerOf10:(short)power withBehavior:(nullable id <NSDecimalNumberBehaviors>)behavior;
-
比較運算
-(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 是 邏輯運算中帶的行為
NSDecimalNumberBehaviors對象可以通過下述方法創建
NSDecimalNumberHandler *roundUp = [NSDecimalNumberHandler
decimalNumberHandlerWithRoundingMode:NSRoundBankers
scale:2
raiseOnExactness:NO
raiseOnOverflow:NO
raiseOnUnderflow:NO
raiseOnDivideByZero:YES];
參數 | 含義 |
---|---|
roundingMode | 四舍五入模式,有四個值: NSRoundUp, NSRoundDown, NSRoundPlain, and NSRoundBankers |
scale | 結果保留幾位小數 |
raiseOnExactness | 發生精確錯誤時是否拋出異常,一般為NO |
raiseOnOverflow | 發生溢出錯誤時是否拋出異常,一般為NO |
raiseOnUnderflow | 發生不足錯誤時是否拋出異常,一般為NO |
raiseOnDivideByZero | 被0除時是否拋出異常,一般為YES |
#枚舉:
NSRoundPlain, // Round up on a tie //四舍五入
NSRoundDown, // Always down == truncate //只舍不入
NSRoundUp, // Always up // 只入不舍
NSRoundBankers 四舍六入, 中間值時, 取最近的,保持保留最后一位為偶數
參照一下圖片, 理解上面枚舉值:
當他們試圖除以0或產生一個數表示太大或太小的時候發生異常。
下面列出了各種異常的名字 表明NSDecimalNumber計算錯誤。
extern NSString *NSDecimalNumberExactnessException; //如果出現一個精確的錯誤
extern NSString *NSDecimalNumberOverflowException; // 溢出
extern NSString *NSDecimalNumberUnderflowException; //下溢
extern NSString *NSDecimalNumberDivideByZeroException; //除數為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數值 floatValue、doubleValue 后依然會出現不精確的問題。
# 其他模式下倒沒有這樣的現象。
.
..
大量使用NSDecimalNumber需要注意的問題
大量NSDecimalNumber 進行計算時比較消耗系統性能,必要時可以使用 C語言級別的NSDecimal 來代替運算,這可以減少不少的系統開銷。NSDecimal是C語言級別的無法直接創建,不幸的是,基礎框架沒有直接創建的方法,你只能先創建生成一個 NSDecimalNumber 再得到對應的 NSDecimal。
# NSDecimal 與 NSDecimalNumber 之間的轉化
NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithString:@"15.99"];
NSDecimal asStruct = [price decimalValue];
NSDecimalNumber *asNewObject = [NSDecimalNumber decimalNumberWithDecimal:asStruct];
NSDecimal的使用中需要注意
C接口使用類似的功能NSDecimalAdd(), NSDecimalSubtract()不是返回結果,這些函數用計算的值填充第一個參數。
這使得可以重用現有NSDecimal的幾個操作,并避免分配不必要的結構只是為了保存中間值。
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));
其他常用數字處理方法
.
# 浮點型小數四舍五入 afterPoint: 小數點后幾位
+(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];
}
# 浮點數處理并去掉多余的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. 以。。。結尾
{
return [str substringToIndex:[str length]-1];//s.substring(0, len - i - 1);
}
else
{
return str;
}
}
# 數字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;
}
小結
數字的處理是及其常見的,本文到此就結束了,后續如有新的歸納會及時更新上來,希望看完這篇文章的朋友能有所收獲。文中如有錯誤,歡迎留言指正。
參考文章:
‘NSDecimalNumber--十進制數’使用方法
NSDecimalNumber
iOS 中的數據結構和算法(一):浮點數
存儲方式