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]];