锻炼吃饭的家伙iOS开发技术面试

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

2016-09-13  本文已影响7703人  何以_aaa

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

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

@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]];
上一篇下一篇

猜你喜欢

热点阅读