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

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.2 1.4 1.3

谢谢一位友友的解惑评论:NSRoundBankers比较特殊,保留位数后一位的数字为5时,根据前一位的奇偶性决定。为偶时向下取正,为奇数时向上取正。如:1.25保留一位小数。5之前是2偶数向下取正1.2;1.35保留一位小数时。5之前为3奇数,向上取正1.4。

代码 :
    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]];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容