编码篇-开发中关于数字的那些事儿

碧波轻舟

前言

在日常的开发中我们随时都会跟数字打着交道,对数字的处理也是很平常的事,本文仅对常用的数字操作一个小结,当一个笔记方便日后查看,也希望读者能从中收获些感觉有用的知识。


本文文章结构

现实中使用数字场景下存在的误差

对于数字要求比较严格的莫过于跟钱有关的 单价、总价等,
亦或者 浮点数在总数中占有的百分比计算,这些都是对价格要求比较严格的,
而使用 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阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容