編碼篇-開發中關于數字的那些事兒

碧波輕舟

前言

在日常的開發中我們隨時都會跟數字打著交道,對數字的處理也是很平常的事,本文僅對常用的數字操作一個小結,當一個筆記方便日后查看,也希望讀者能從中收獲些感覺有用的知識。


本文文章結構

現實中使用數字場景下存在的誤差

對于數字要求比較嚴格的莫過于跟錢有關的 單價、總價等,
亦或者 浮點數在總數中占有的百分比計算,這些都是對價格要求比較嚴格的,
而使用 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.25
    2=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 在計算機的實際存儲中是這樣存儲的

單精度浮點數8.25的存儲方式

其中float的存儲方式如下圖所示:


float類型的存儲方式

而 double 的存儲方式為:


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精度較高,所以會比基本數據類型費時,所以需要權衡考慮,
不過蘋果官方建議在貨幣以及要求精度很高的場景下使用。

作為NSNumber的子類stringValue doubleValue自然是自帶方法
科學計數法

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 中的數據結構和算法(一):浮點數
存儲方式

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容