NSDecimalNumber的用法
在iOS开发中,和货币价格计算相关的,需要注意计算精度的问题。即使只是两位小数,也会出现误差。使用float类型运算,是完全不够的。做电商项目的时候遇到一个问题,就是精确价格的问题,许多电商项目都会涉及到,就是当我们对价格用float或者double进行处理的时候,有的时候总是会出现一些误差,还有就是假如我们精确小数点后面两位,就有可能出现xxx.00或者xx.x0的问题,这些都是我们在处理价格的时候可能会遇到的坑!!而且涉及到钱的问题,不管什么app都要做到特别的精确,不能出现任何的误差!
举🌰
float a = 0.01;
int b = 99999999;
double c = 0.0;
c = a*b;
NSLog(@"%f",c); //输出结果为 1000000.000000
NSLog(@"%.2f",c); //输出结果为 1000000.00
解决方案1:
c = a*(double)b;
NSLog(@"%f",c); //输出结果 999999.967648
NSLog(@"%.2f",c); //输出结果 999999.97
// 失败😶😶😶😶😶😶
解决方案2:
NSString *objA = [NSString stringWithFormat:@"%.2f", a];
NSString *objB = [NSString stringWithFormat:@"%.2f", (double)b];
c = [objA doubleValue] * [objB doubleValue];
NSLog(@"%.2f",c); //输出结果 999999.99
//✌️✌️✌️✌️✌️✌️
//计算的结果还是比较准确的,不过需要做格式化输入和格式化输出的处理。同时使用NSString来转换,这样的写法看起来比较奇怪。
解决方案3:
NSString *decimalNumberMutiplyWithString(NSString *multiplierValue,NSString*multiplicandValue) {
NSDecimalNumber *multiplierNumber = [NSDecimalNumberdecimalNumberWithString:multiplierValue];
NSDecimalNumber *multiplicandNumber = [NSDecimalNumberdecimalNumberWithString:multiplicandValue];
NSDecimalNumber *product = [multiplicandNumberdecimalNumberByMultiplyingBy:multiplierNumber];
return [product stringValue];
}
NSLog(@"%@",decimalNumberMutiplyWithString([NSString stringWithFormat:@"%f",a], [NSString stringWithFormat:@"%d",b]));
//👍👍👍👍👍👍
//通过NSDecimalNumber提供的计算方式,可以很好的计算出准确的精度的数据,同时不需要使用格式化输出等。其计算的精度是比较高,这是官方建议的货币计算的API,对乘除等计算都有单独的API接口来提供。
下面开始讲解这个NSDecimalNumber
NSDecimalNumber这个类为OC程序提供了定点算法功能,它被设计为了不会损失精度并且可预先设置凑整规则的10进制计算,这让它成为一个比浮点数(double)更好的选则去表示货币,然而作为交换用NSDecimalNumber计算变得更加复杂。
![](https://img.haomeiwen.com/i1414870/b5d35d09d5c9d954.png)
在内部,一个有小数点的数被表示为上图中的这种形式,这个符号定义了它是正数还是负数,这个尾数是一个无符号的整数用来表示有效数字,这个指数决定了小数点在尾数中的位置。
NSDecimalNumber可以通过传递尾数、指数、符号来组装,但是从一个字符串转换成一个NSDecimalNumber更容易,以下代码分别用两种方式创建了值15.99。
NSDecimalNumber *price;
price = [NSDecimalNumber decimalNumberWithMantissa:1599
exponent:-2
isNegative:NO];
price = [NSDecimalNumber decimalNumberWithString:@"15.99"];
像NSNumber一样,所有的NSDecimalNumber对象都是不可变额,这意味着在它们创建之后不能改变它们的值
NSDecimalNumber的计算
NSDecimalNumber的主要工作是提供可供选择的定点算法给C的原生算法操作,全部的五个NSDecimalNumber的计算方法在下面被演示:
NSDecimalNumber *price1 = [NSDecimalNumber decimalNumberWithString:@"15.99"];
NSDecimalNumber *price2 = [NSDecimalNumber decimalNumberWithString:@"29.99"];
NSDecimalNumber *coupon = [NSDecimalNumber decimalNumberWithString:@"5.00"];
NSDecimalNumber *discount = [NSDecimalNumber decimalNumberWithString:@".90"];
NSDecimalNumber *numProducts = [NSDecimalNumber decimalNumberWithString:@"2.0"];
NSDecimalNumber *subtotal = [price1 decimalNumberByAdding:price2];
NSDecimalNumber *afterCoupon = [subtotal decimalNumberBySubtracting:coupon];
NSDecimalNumber *afterDiscount = [afterCoupon decimalNumberByMultiplyingBy:discount];
NSDecimalNumber *average = [afterDiscount decimalNumberByDividingBy:numProducts];
NSDecimalNumber *averageSquared = [average decimalNumberByRaisingToPower:2];
NSLog(@"Subtotal: %@", subtotal); // 45.98
NSLog(@"After coupon: %@", afterCoupon); // 40.98
NSLog((@"After discount: %@"), afterDiscount); // 36.882
NSLog(@"Average price per product: %@", average); // 18.441
NSLog(@"Average price squared: %@", averageSquared); // 340.070481
不像它们的相对物浮点,这些操作保证了精确性,然而,你会注意到有很多超出计算结果的额外小数位,根据这个应用,它们可能不令人满意(例如,你可能想约束货币值只有2个小数位),这是为什么自定义进位行为被引入的原因。
Rounding Behavior
![](https://img.haomeiwen.com/i1414870/7602dc021d3fc75b.png)
每一个在上文中的计算方法有一个替换物---behavior:下面列出了让你定义这个操作凑整这个结果的值,这个类封装了一个特别的凑整行为,可以被实例化如下:
NSDecimalNumberHandler *roundUp = [NSDecimalNumberHandler
decimalNumberHandlerWithRoundingMode:NSRoundUp
scale:2
raiseOnExactness:NO
raiseOnOverflow:NO
raiseOnUnderflow:NO
raiseOnDivideByZero:YES];
NSRoundUp属性使所有的操作算到最近的位置,其他的进位选项是NSRoundPlain, NSRoundDown, 和 NSRoundBankers,它们都被定义在NSRoundingMode,scale参数定义了结果值保留的小数位的数量,其余的参数给所有的操作定义了异常处理行为,这这个例子中,NSDecimalNumber将只捕获一个异常,如果你尝试除0。
这个凑整的行为可以在之后被调用通过decimalNumberByMultiplyingBy:withBehavior:这个方法(或者任何其他的计算方法),如下所示.
NSDecimalNumber *subtotal = [NSDecimalNumber decimalNumberWithString:@"40.98"];
NSDecimalNumber *discount = [NSDecimalNumber decimalNumberWithString:@".90"];
NSDecimalNumber *total = [subtotal decimalNumberByMultiplyingBy:discount
withBehavior:roundUp];
NSLog(@"Rounded total: %@", total);
//现在,代替36.882,这个total算到2个小数位,结果是36.89
Comparing NSDecimalNumbers
像NSNumber, NSDecimalNumber对象应该用compare:方法代替原生的不等式操作,此外,这确保了值被比较,即使他们存储于不通的实例中,例如
NSDecimalNumber*discount1 = [NSDecimalNumber decimalNumberWithString:@".85"];
NSDecimalNumber*discount2 = [NSDecimalNumber decimalNumberWithString:@".9"];
NSComparisonResult result = [discount1 compare:discount2];
if (result ==NSOrderedAscending) {
NSLog(@"85%% < 90%%小于");
} else if (result == NSOrderedSame) {
NSLog(@"85%% == 90%%等于");
} else if (result ==NSOrderedDescending) {
NSLog(@"85%% > 90%%大于");
}
NSDecimalNumber也从NSNumber中继承了isEqualToNumber:
Decimal Numbers in C
对于大多数实用的目的,NSDecimalNumber应该能满足你定点的需要。不过也有一个基于纯C语言的基础函数,它提供了一种更高效率的方式。在下面的讨论中,因此我们优先选择它,高效实现一个大数的计算。
NSDecimal
代替NSDecimalNumber对象,C实例创建了一个NSDecimal结构体,不幸的是Foundation Framework没有使它很容易的创建,你需要先生成一个NSDecimalNumber,再用它的decimalValue方法获取到一个NSDecimal。而从NSDecimal可以用它是的工厂方法直接获取到一个NSDecimalNumber,也被展示如下
NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithString:@"15.99"];
NSDecimal asStruct = [price decimalValue];
NSDecimalNumber *asNewObject = [NSDecimalNumber decimalNumberWithDecimal:asStruct];
以下的代码结果变量被函数调用了5次,和算法节每一次计算都创建一个NSDecimalNumber做比较,
NSDecimal price1 = [[NSDecimalNumber decimalNumberWithString:@"15.99"] decimalValue];
NSDecimal price2 = [[NSDecimalNumber decimalNumberWithString:@"29.99"] decimalValue];
NSDecimal coupon = [[NSDecimalNumber decimalNumberWithString:@"5.00"] decimalValue];
NSDecimal discount = [[NSDecimalNumber decimalNumberWithString:@".90"] decimalValue];
NSDecimal numProducts = [[NSDecimalNumber decimalNumberWithString:@"2.0"] decimalValue]
NSLocale *locale = [NSLocale currentLocale];
NSDecimal result;
NSDecimalAdd(&result, &price1, &price2, NSRoundUp);
NSLog(@"Subtotal: %@", NSDecimalString(&result, locale));
NSDecimalSubtract(&result, &result, &coupon, NSRoundUp);
NSLog(@"After coupon: %@", NSDecimalString(&result, locale));
NSDecimalMultiply(&result, &result, &discount, NSRoundUp);
NSLog(@"After discount: %@", NSDecimalString(&result, locale));
NSDecimalDivide(&result, &result, &numProducts, NSRoundUp);
NSLog(@"Average price per product: %@", NSDecimalString(&result, locale));
NSDecimalPower(&result, &result, 2, NSRoundUp);
NSLog(@"Average price squared: %@", NSDecimalString(&result, locale));
//这些函数接受一个NSDecimal结构体的引用,这是为什么我们需要用一个取址符(&)代替直接使用它们,同时凑整是每一个操作固有的一部分,它没有像NSDecimalNumberHandler被封装在一个分开的单独实体中。
Error Checking
和面向对象编程的方法不同,这个计算函数在计算错误发生时不会捕获异常,代替的是,它们允许普通的C模式用一个返回值去表明成功或者失败,所有上文的函数返回了一个NSCalculationError,它定义了发生了什么错误,这个可能的情景如下:
NSDecimal a = [[NSDecimalNumber decimalNumberWithString:@"1.0"] decimalValue];
NSDecimal b = [[NSDecimalNumber decimalNumberWithString:@"0.0"] decimalValue];
NSDecimal result;
NSCalculationError success = NSDecimalDivide(&result, &a, &b, NSRoundPlain);
switch (success) {
case NSCalculationNoError:
NSLog(@"Operation successful");
break;
case NSCalculationLossOfPrecision:
NSLog(@"Error: Operation resulted in loss of precision");
break;
case NSCalculationUnderflow:
NSLog(@"Error: Operation resulted in underflow");
break;
case NSCalculationOverflow:
NSLog(@"Error: Operation resulted in overflow");
break;
case NSCalculationDivideByZero:
NSLog(@"Error: Tried to divide by zero");
break;
default:
break;
}
Comparing NSDecimals
NSDecimal discount1 = [[NSDecimalNumber decimalNumberWithString:@".85"] decimalValue];
NSDecimal discount2 = [[NSDecimalNumber decimalNumberWithString:@".9"] decimalValue];
NSComparisonResult result = NSDecimalCompare(&discount1, &discount2);
if (result == NSOrderedAscending) {
NSLog(@"85%% < 90%%");
} else if (result == NSOrderedSame) {
NSLog(@"85%% == 90%%");
} else if (result == NSOrderedDescending) {
NSLog(@"85%% > 90%%");
}