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]];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容