将double转换为NSDecimalNumber并保持精度

3

我需要将在 double 中执行的计算结果转换,但我不能使用 decimalNumberByMultiplyingBy 或任何其他 NSDecimalNumber 函数。我尝试了以下几种方式来获得准确的结果:

double calc1 = 23.5 * 45.6 * 52.7;  // <-- Correct answer is 56473.32
NSLog(@"calc1 = %.20f", calc1);

-> calc1 = 56473.32000000000698491931

NSDecimalNumber *calcDN = (NSDecimalNumber *)[NSDecimalNumber numberWithDouble:calc1];
NSLog(@"calcDN = %@", [calcDN stringValue]);

-> calcDN = 56473.32000000001024

NSDecimalNumber *testDN = [[[NSDecimalNumber decimalNumberWithString:@"23.5"] decimalNumberByMultiplyingBy:[NSDecimalNumber decimalNumberWithString:@"45.6"]] decimalNumberByMultiplyingBy:[NSDecimalNumber decimalNumberWithString:@"52.7"]];
NSLog(@"testDN = %@", [testDN stringValue]);

-> 测试DN = 56473.32

我知道这个差异与各自的精度有关。

但是我的问题是:无论double的初始值是什么,如何以最准确的方式将此数字四舍五入?如果存在更准确的方法来进行初始计算,那么是什么方法?

2个回答

4

你可以使用 double 来表示数字并容忍不精确性,或者使用其他数字表示方法,例如 NSDecimalNumber。这完全取决于所期望的值和关于准确性的业务需求。

如果真的不能使用由 NSDecimalNumber 提供的算术方法,则最好使用 NSDecimalNumberHandler 控制舍入行为,它是 NSDecimalNumberBehaviors 协议的一个具体实现。实际的舍入是通过 decimalNumberByRoundingAccordingToBehavior: 方法执行的。

下面是代码片段 - 它是用 Swift 编写的,但应该很容易阅读:

let behavior = NSDecimalNumberHandler(roundingMode: NSRoundingMode.RoundPlain,
                                             scale: 2,
                                  raiseOnExactness: false,
                                   raiseOnOverflow: false,
                                  raiseOnUnderflow: false,
                               raiseOnDivideByZero: false)

let calcDN : NSDecimalNumber = NSDecimalNumber(double: calc1)
                               .decimalNumberByRoundingAccordingToBehavior(behavior)
calcDN.stringValue // "56473.32"

我不知道在使用double表示时如何提高实际计算的准确性。


谢谢,但我没有固定的小数位数。我正在寻求最大精度。使用10位比例尺?但如果小数点前有很多数字,它肯定会改变。 - Croises
哦,我明白了。所以您想在计算之前估计任意“double”数字的舍入误差大小,以便知道在小数点后截断的位置?嗯,我不知道任何这方面的方法,而且它似乎比使用替代数字表示法要复杂得多。 - siejkowski
我有很多三角函数的计算,从一个计算转到另一个计算很困难。但是对于简单的计算,我会尝试去做。我真的希望能够消除可见的错误。 - Croises
1
哦,如果你正在进行三角函数计算,那么也许使用math.h中的函数或Accelerate框架会帮助你?请参阅https://developer.apple.com/library/ios/documentation/Accelerate/Reference/AccelerateFWRef/_index.html。 - siejkowski
是的,我已经使用了 math.h 来完成那个。我的意思是说,如果不是被迫这样做,我会避免使用 double。(我不知道我是否表达清楚,因为我的英语非常不完美,也达到了准确度的极限;-) 抱歉!) - Croises

3

我建议根据你的double数字的位数对数字进行四舍五入,这样NSDecimalNumber将被截断为仅显示适当数量的数字,从而消除由潜在误差形成的数字,例如:

// Get the number of decimal digits in the double
int digits = [self countDigits:calc1];

// Round based on the number of decimal digits in the double
NSDecimalNumberHandler *behavior = [NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:NSRoundDown scale:digits raiseOnExactness:NO raiseOnOverflow:NO raiseOnUnderflow:NO raiseOnDivideByZero:NO];
NSDecimalNumber *calcDN = (NSDecimalNumber *)[NSDecimalNumber numberWithDouble:calc1];
calcDN = [calcDN decimalNumberByRoundingAccordingToBehavior:behavior];

我已经从这个答案中适应了countDigits:方法:

- (int)countDigits:(double)num {
    int rv = 0;
    const double insignificantDigit = 18; // <-- since you want 18 significant digits
    double intpart, fracpart;
    fracpart = modf(num, &intpart); // <-- Breaks num into an integral and a fractional part.

    // While the fractional part is greater than 0.0000001f,
    // multiply it by 10 and count each iteration
    while ((fabs(fracpart) > 0.0000001f) && (rv < insignificantDigit)) {
        num *= 10;
        fracpart = modf(num, &intpart);
        rv++;
    }
    return rv;
}

我进行了一些测试。并且使用 23.5 * 45.6 * 52.7 完美运作。但是对于 2.35 * 45.6 * 52.7 就不是这样了。我们必须用 2.35 改变 insignificantDigit = 11。我不明白为什么会这样。这肯定与结果的二进制表示有关。 - Croises
@Croises,这对我有效...56112.852。您期望什么结果? - Lyndsey Scott
使用insignificantDigit = 18,我的结果是5647.332000000001 - Croises
是的,使用 insignificantDigit 18,我的结果是 56112.852。你是如何打印 calcDN 的:NSLog(@"%@", calcDN); - Lyndsey Scott
1
我不能对所有的计算都使用(decimalNumberByMultiplyingBy :),因为我有很多复杂的计算(三角函数等)。但我会在这个方向上做出努力。我想现在暂时会采用你的解决方案(我加了+1)。如果有更好的解决方案提出来,允许我保持开放几周时间观察。我有权做梦,因为这是我遇到了帮我解决问题的仙女的季节。再次感谢! - Croises
显示剩余15条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接