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

碧波轻舟

前言

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


本文文章结构

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

对于数字要求比较严格的莫过于跟钱有关的 单价、总价等, 亦或者 浮点数在总数中占有的百分比计算,这些都是对价格要求比较严格的, 而使用 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 在计算机的实际存储中是这样存储的

单精度浮点数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 中的数据结构和算法(一):浮点数 存储方式

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小樱的经验随笔

POJ 2492 A Bug's Life

A Bug's Life Time Limit: 10000MS Memory Limit: 65536K Total Submissions:...

28710
来自专栏我是攻城师

Fastjson解析嵌套Map例子

3835
来自专栏数据结构与算法

P2580 于是他错误的点名开始了

题目背景 XS中学化学竞赛组教练是一个酷爱炉石的人。 他会一边搓炉石一边点名以至于有一天他连续点到了某个同学两次,然后正好被路过的校长发现了然后就是一顿欧拉欧拉...

2977
来自专栏码匠的流水账

聊聊storm TridentTopology的构建

storm-core-1.2.2-sources.jar!/org/apache/storm/trident/TridentTopology.java

1002
来自专栏Albert陈凯

2018-04-17 Java的Collection集合类3分钟搞掂Set集合前言

3分钟搞掂Set集合 前言 声明,本文用的是jdk1.8 现在这篇主要讲Set集合的三个子类: HashSet集合 A:底层数据结构是哈希表(是一个元素为链...

2977
来自专栏程序你好

如何使用Java Stream Collectors(归约器)?

Java 8引入了Stream API,它允许我们以声明的方式处理数据。此外,Stream还可以在不需要编写多线程代码的情况下使用多核架构。

801
来自专栏码匠的流水账

聊聊storm TridentTopology的构建

storm-core-1.2.2-sources.jar!/org/apache/storm/trident/TridentTopology.java

1803
来自专栏calmound

UVA 10604 Chemical Reaction(六维dp数组)

题意:有六种不同的试剂,放于试管中,不同的试剂融合其产生的热量不同,且生成的新试剂也不相同,问最后最低温度是多少。 分析:由于只有六种试剂,所以开辟一个六维dp...

3637
来自专栏noteless

[四] java8 函数式编程 收集器浅析 收集器Collector常用方法 运行原理 内部实现

收集器是由四个函数约定构成,它们一起工作,将条目汇集到一个可变的结果容器中,并可选择性地对结果执行最终转换。

2472
来自专栏五分钟学算法

LeeCode题目图解

There is an English version of README here. just click it!

1602

扫码关注云+社区

领取腾讯云代金券