美文网首页锻炼吃饭的家伙iOS开发技术面试
iOS 浮点数的精确计算和四舍五入问题

iOS 浮点数的精确计算和四舍五入问题

作者: 何以_aaa | 来源:发表于2016-09-13 12:33 被阅读7703次

iOS开发中,使用浮点数(float,double)类型运算需要注意计算精度的问题。即使只是两位小数,也会出现误差。一般和货币价格计算相关的更应注意。
项目中遇到的问题:后台返回float a;需要快速从0依次累加一个值显示到a,例如a/10,共显示10次。遇到的问题包括:

  • 最后计算值有误差(与a有差距)
  • 最后显示的小数点位数

首先简单贴一下定时器代码:

@property (nonatomic, strong, readonly) CADisplayLink *countDownTimer;
- (void)start
{
    if (!_countDownTimer) {
        _countDownTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(countDown)];
        _countDownTimer.frameInterval = 1.;
    }
    
    [_countDownTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)countDown
{
    _ascending = (_endNumber > _currentNumber);
    NSInteger interval = ABS(_currentNumber - _endNumber);
    NSInteger c = 0;
    if (_countInterval > interval) {
        c = interval;
    }
    else {
        c = _countInterval > 0 ? _countInterval : (int)sqrtf(interval);
    }

    self.currentNumber = _ascending ? _currentNumber + c : _currentNumber - c;
    self.text = [NSString stringWithFormat:@"%li",(long)_currentNumber];
    
    if (self.countDownHandeler) {
        self.countDownHandeler(self,_currentNumber,(_currentNumber == _endNumber));
    }
    
    if (_currentNumber == _endNumber) {
        [_countDownTimer invalidate];
        _countDownTimer = nil;
    }
}

精确计算处理方案:

1. 将float强制转换为double(依旧会丢失精度)

    float a = 0.01;
    int b = 9999;
    double c = (double)a*(double)b;
    NSLog(@"%f",c);     //输出结果为 99.989998
    NSLog(@"%.2f",c);   //输出结果为 99.99

2. 将原始类型强制转换为纯粹的double, 再通过和NSString转换(可保留精度)

    float a = 0.001;
    int b = 999999;
    NSString *objA = [NSString stringWithFormat:@"%.3f", (double)a];
    NSString *objB = [NSString stringWithFormat:@"%.2f", (double)b];

    double c = [objA doubleValue] * [objB doubleValue];
    NSLog(@"%f",c);     //输出结果为 999.999000
    NSLog(@"%.3f",c);   //输出结果为 999.999

3.使用NSDecimalNumber类(推荐!!!)

NSDecimalNumber为OC程序提供了定点算法功能,为了不损失精度设置为可预先设置凑整规则的10进制计算,因此对于要求更高的货币计算应该使用这个类,而不是浮点数(double)。
像NSNumber一样,所有的NSDecimalNumber对象都是不可变的,这意味着在它们创建之后不能改变它们的值。

3.1 基本使用:

首先介绍一个重要的参数 NSDecimalNumberHandler ,它决定了四舍五入的模式及结果保留几位小数。

参数 含义
roundingMode 四舍五入模式,有四个值: NSRoundUp, NSRoundDown, NSRoundPlain, and NSRoundBankers
scale 结果保留几位小数
raiseOnExactness 发生精确错误时是否抛出异常,一般为NO
raiseOnOverflow 发生溢出错误时是否抛出异常,一般为NO
raiseOnUnderflow 发生不足错误时是否抛出异常,一般为NO
raiseOnDivideByZero 被0除时是否抛出异常,一般为YES
参数 含义 value1 value2 value3 value4 value5
OriginValue 原始数值 1.2 1.21 1.25 1.35 1.27
NSRoundPlain 貌似取整 1.2 1.2 1.3 1.4 1.3
NSRoundDown 只舍不入 1.2 1.2 1.2 1.3 1.2
NSRoundUp 只入不舍 1.2 1.3 1.3 1.4 1.3
NSRoundBankers 四舍五入 1.2 1.2 1.3 1.4 1.3
代码 :
    NSDecimalNumberHandler *roundUp = [NSDecimalNumberHandler
                                       decimalNumberHandlerWithRoundingMode:NSRoundDown
                                       scale:2
                                       raiseOnExactness:NO
                                       raiseOnOverflow:NO
                                       raiseOnUnderflow:NO
                                       raiseOnDivideByZero:YES];

    NSDecimalNumber *a = [NSDecimalNumber decimalNumberWithString:@"29.99"];
    NSDecimalNumber *b = [NSDecimalNumber decimalNumberWithString:@"15.998"];
    NSDecimalNumber *c = [NSDecimalNumber decimalNumberWithString:@"5.01"];
    
    // 和
    NSDecimalNumber *sum = [a decimalNumberByAdding:b];
    // 差
    NSDecimalNumber *subtract = [a decimalNumberBySubtracting:b];
    // 乘积
    NSDecimalNumber *multiply = [a decimalNumberByMultiplyingBy:b];
    // 除
    NSDecimalNumber *divide = [a decimalNumberByDividingBy:b];
    // n次方
    NSDecimalNumber *squared = [c decimalNumberByRaisingToPower:2];
    // 指数运算
    NSDecimalNumber *xx = [c decimalNumberByMultiplyingByPowerOf10:2];
    // 四舍五入
    NSDecimalNumber *yy = [b decimalNumberByRoundingAccordingToBehavior:roundUp];
    
    NSLog(@"和: %@", sum);          // 和: 45.988
    NSLog(@"差: %@", subtract);     // 差: 13.992
    NSLog(@"积: %@", multiply);     // 积: 479.78002
    NSLog(@"除: %@", divide);       // 除: 1.8746093261657707213401675209401175146
    NSLog(@"n次方: %@", squared);   // n次方: 25.1001
    NSLog(@"指数: %@", xx);         // 指数: 501
    NSLog(@"四舍五入: %@", yy);      // 四舍五入: 15.99```

能直接决定计算结果的小数位数,及四舍五入模式:
    // 乘积
    NSDecimalNumber *multiply = [a decimalNumberByMultiplyingBy:b withBehavior:roundUp];   
    //积: 479.78
3.2 比较:

像NSNumber, NSDecimalNumber对象应该用compare:方法代替原生的不等式(==)操作,这确保了即使他们存储于不通的实例中也是 值被比较 , 例如

    NSDecimalNumber *num1 = [NSDecimalNumber decimalNumberWithString:@".85"];
    NSDecimalNumber *num2 = [NSDecimalNumber decimalNumberWithString:@".9"];
    NSComparisonResult result = [num1 compare:num2];
    
    if (result == NSOrderedAscending) {
        NSLog(@"85%% < 90%% 小于");
    } else if (result == NSOrderedSame) {
        NSLog(@"85%% == 90%% 等于");
    } else if (result == NSOrderedDescending) {
        NSLog(@"85%% > 90%% 大于");
    }
    // 85% < 90% 小于

回到项目中遇到的问题:

控制每次显示的小数点位数:

因为每次的精确计算都是累加,所以在给UI控件赋值的时候,获得NSDecimalNumber的值,通过NSString取出,并转换成float,进行控制小数位数的显示。
因为如果每次累加都使用roundUp来控制结果,那么上次计算的四舍五入的误差就会计算到下次的累加中,这样最终10次累加后的结果就不精确了。

    NSDecimalNumber *sumNum = [currentNumber decimalNumberByAdding:cNumber];
    NSString *str = sumNum.stringValue;
    self.label.text =  [NSString stringWithFormat:@"%.2f人",[str floatValue]];

相关文章

网友评论

  • 隔壁_王叔叔:NSRoundBankers比较特殊,保留位数后一位的数字为5时,根据前一位的奇偶性决定。为偶时向下取正,为奇数时向上取正。如:1.25保留1为小数。5之前是2偶数向下取正1.2;1.35保留1位小数时。5之前为3奇数,向上取正1.4
    何以_aaa:谢谢,已注释
  • coderChrisLee:真奇怪,我在另外博客里面看到NSRoundPlain是四舍五入,而你这边说NSRoundBankers是四舍五入。苹果官方文档里面经NSRoundPlain后1.25得到的是1.3,而经NSRoundBankers后得到的是1.2。我验证了下,苹果文档的注释没毛病,的确NSRoundPlain才是四舍五入。你这篇文档NSRoundBankers,1.25得到的值也是1.3,写错了吧,岂不是跟NSRoundPlain全部一样了?
  • TIME_for:NSRoundPlain NSRoundBankers,好好看看这两个区别耶。
    // Rounding policies :
    // Original
    // value 1.2 1.21 1.25 1.35 1.27
    // Plain 1.2 1.2 1.3 1.4 1.3
    // Down 1.2 1.2 1.2 1.3 1.2
    // Up 1.2 1.3 1.3 1.4 1.3
    // Bankers 1.2 1.2 1.2 1.4 1.3
    这是苹果介绍 NSRoundingMode 时给的Demo,和你自己写的对比下。写文章最好是仔细点,不要误导读者啊。
    Lucus_Linx:Bankers 四舍五入,按照我们常规的1.25 = 1.3 啊,没毛病,苹果给的为什么是1.2 ?????
  • 1b06c44ac991:表格里一个四舍五入的值你写错了
    何以_aaa:是吗,欢迎指出
  • Rejected:兄弟你这个不靠谱啊:unamused:
    何以_aaa:@Rejected 欢迎指正

本文标题:iOS 浮点数的精确计算和四舍五入问题

本文链接:https://www.haomeiwen.com/subject/xprmettx.html