前言
在日常的开发中我们随时都会跟数字打着交道,对数字的处理也是很平常的事,本文仅对常用的数字操作一个小结,当一个笔记方便日后查看,也希望读者能从中收获些感觉有用的知识。
现实中使用数字场景下存在的误差
对于数字要求比较严格的莫过于跟钱有关的 单价、总价等,
亦或者 浮点数在总数中占有的百分比计算,这些都是对价格要求比较严格的,
而使用 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 中的数据结构和算法(一):浮点数
存储方式