问题引出

在开发中,涉及价格金额处理,后台会返回Number类型的数据,打印或者经过Json转Model后的NSString可能出现精度丢失的问题,如果涉及到金额的加减乘除运算问题将暴露得更为明显。这里就iOS数据精度处理做一个总结。

问题复现

NSNumber转NSSting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
NSArray *numbers = @[
                     @99.00,
                     @99.09,
                     @99.19,
                     @99.29,
                     @99.39,
                     @99.49,
                     @99.59,
                     @99.69,
                     @99.79,
                     @99.89,
                     @99.99,
                     ];

for (int i = 0; i < numbers.count; i++) {
    NSNumber *number = numbers[i];
    NSString *strValue = [number stringValue];

    NSLog(@"strValue:%@",strValue);
}

/*
 oldVlue:99.00 strValue:99
 oldVlue:99.09 strValue:99.09
 oldVlue:99.19 strValue:99.19
 oldVlue:99.29 strValue:99.29000000000001
 oldVlue:99.39 strValue:99.39
 oldVlue:99.48 strValue:99.48999999999999
 oldVlue:99.59 strValue:99.59
 oldVlue:99.69 strValue:99.69
 oldVlue:99.79 strValue:99.79000000000001
 oldVlue:99.89 strValue:99.89
 oldVlue:99.99 strValue:99.98999999999999
 */

在这里我们发现将NSNumber转换成NSSting的过程中可能会出现精度丢失。

Json到Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//出现BUG的条件必须是两位数,且带两位小数,类型还必须是float
//两位数:十位必须是7、8、9;个位数随意
//两位小数:个位数随意;十位数必须是0
NSString *jsonStr = @"{\"71.40\":71.40, \"97.40\":97.40, \"80.40\":80.40, \"188.40\":188.40}";
NSLog(@"json:%@", jsonStr);

NSData *jsonData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
NSError *jsonParsingError = nil;
NSDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:[NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonParsingError]];

NSLog(@"dic:%@", dic);
/*
2017-10-14 18:29:19.434 FloatTransferDemo[62722:3093992] dic:{
    "188.40" = "188.4";
    "71.40" = "71.40000000000001";
    "80.40" = "80.40000000000001";
    "97.40" = "97.40000000000001";
}
*/

在这里我们发现将Json解析成Model的过程中可能会出现精度丢失。

问题分析

因为浮点数在计算机中是采用IEEE规定的标准浮点格式,即二进制科学表示法。 在这种表示法中,一个数 S = M * 2 ^ N

其中N表示阶码,M表示位数(有效数字位)。 例如一个float类型的浮点,在32bit位上,占4个字节,字节表示为

【31】N:【30 ~ 23】 M:【22~0】
  • 31位表示符号位: 0正,1负
  • 中间8位是阶码位: 表示范围【-128 ~ 127】,对于float类型数据规定其偏移量为127
  • 后面23位是有效数字位: 因为科学计数法,整数位定死了是1,所以这里记录的是小数点后面的二进制为

指数N决定它的范围,因为M总是一个以1开头的小数,以float来说即是:-2 ^ 128 ~ 2 ^ 128,即float能表示的数的大小的范围。

而它的精度是由位数(也就是有效的数据位)来决定的, 2 ^ 23 = 8388608,总共7位,表示最多能用7位有效数字,最多能表示到.8388708即小数点后7位,由于不能完全表示全部的7位数,所以它的精度范围是6位~7位。

同理可得double的精度是2 ^ 52 = 4503599627370496, 共16位,所以精度为15 ~ 16位。

总结:float/double类型的范围和精度的计算方式

不同机器字节序的规定
公式: S = M * 2 ^ N
二进制在内存中是以补码形式存储,负数要对其二进制绝对值按位取反再加一,正数的补码与原码形式相同

也就是说float和doublel类型数据在计算机中存储可能是不精确的。 当我们需要转换成浮点类型是数据时,最好用double,因为double的精度更高,出现丢精度的概率相对是较小的。

在iOS中提供一个专用的类来处理浮点数据相关的运算:NSDecimalNumber

解决方案

使用NSDecimalNumber来进行浮点数处理。 我们给NSString添加一个分类来处理浮点运算问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
///.h
@interface NSString (DecimalNumber)

+ (NSString *)decimalNumberWithNSNumber:(NSNumber * )number;
+ (NSString *)decimalNumberWithDouble:(double)conversionValue;

@end

///.m
#import "NSString+DecimalNumber.h"

@implementation NSString (DecimalNumber)

+ (NSString *)decimalNumberWithNSNumber:(NSNumber * )number
{
    double conversionValue = [number doubleValue];
    NSString *doubleString        = [NSString stringWithFormat:@"%lf", conversionValue];
    NSDecimalNumber *decNumber    = [NSDecimalNumber decimalNumberWithString:doubleString];
    return [decNumber stringValue];
}

+ (NSString *)decimalNumberWithDouble:(double)conversionValue
{
    NSString *doubleString        = [NSString stringWithFormat:@"%lf", conversionValue];
    NSDecimalNumber *decNumber    = [NSDecimalNumber decimalNumberWithString:doubleString];
    return [decNumber stringValue];
}

@end

如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
NSArray *numbers = @[
                     @99.00,
                     @99.09,
                     @99.19,
                     @99.29,
                     @99.39,
                     @99.49,
                     @99.59,
                     @99.69,
                     @99.79,
                     @99.89,
                     @99.99,
                     ];

for (int i = 0; i < numbers.count; i++) {
    NSNumber *number = numbers[i];
    NSString *strValue = [NSString decimalNumberWithNSNumber:number];

    NSLog(@"strValue:%@",strValue);
}

/*
 oldVlue:99.00 strValue:99
 oldVlue:99.09 strValue:99.09
 oldVlue:99.19 strValue:99.19
 oldVlue:99.29 strValue:99.29
 oldVlue:99.39 strValue:99.39
 oldVlue:99.48 strValue:99.49
 oldVlue:99.59 strValue:99.59
 oldVlue:99.69 strValue:99.69
 oldVlue:99.79 strValue:99.79
 oldVlue:99.89 strValue:99.89
 oldVlue:99.99 strValue:99.99
 */

问题得以解决。☕️

参考资料:

Comments