3

I need to convert the results of calculations performed in a double, but I cannot use decimalNumberByMultiplyingBy or any other NSDecimalNumber function. I've tried to get an accurate result in the following ways:

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

-> testDN = 56473.32

I understand that this difference is related to the respective accuracies.

But here's my question: How can I round this number in the most accurate way possible regardless of what the initial value of double may be? And if a more accurate method exists to do the initial calculation, what is that method?

Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
Croises
  • 18,570
  • 4
  • 30
  • 47

2 Answers2

4

Well, you can either use double to represent the numbers and embrace inaccuracies or use some different number representation, such as NSDecimalNumber. It all depends on what are the expected values and business requirements concerning accuracy.

If it is really crucial not to use arithmetic methods provided by NSDecimalNumber, than the rounding behaviour is best controlled using NSDecimalNumberHandler, which is a concrete implementation of NSDecimalNumberBehaviors protocol. The actual rounding is performed using decimalNumberByRoundingAccordingToBehavior: method.

Here comes the snippet - it's in Swift, but it should be readable:

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"

I do not know of any method of improving the accuracy of the actual computations when using double representation.

siejkowski
  • 1,621
  • 12
  • 11
  • Thank you, but I do not have a fixed number of decimals. I am looking for maximum accuracy. With scale 10 ? But it certainly changes if I have a large number before the decimal point. – Croises Dec 25 '14 at 16:03
  • Oh, I see. So you'd like to be able to estimate how big the rounding error is for the arbitrary `double` numbers before the computations, so you know where to truncate after decimal points? Well, I do not know any method for that, and it seems way more complicated than using alternative number representation. – siejkowski Dec 25 '14 at 16:21
  • I have many trigonometric calculations, And it is difficult to switch from one to another. But I'll try to do it for simple calculations, as in my example. I'd really like to get to do is to remove the visible errors. – Croises Dec 25 '14 at 16:26
  • 1
    Oh, if you're doing the trigonometric calculations, than maybe using the functions from `math.h` or the *Accelerate framework* are gonna help you? See https://developer.apple.com/library/ios/documentation/Accelerate/Reference/AccelerateFWRef/_index.html – siejkowski Dec 25 '14 at 16:34
  • Yes, I already use `math.h` for that. What I meant to say is that I would avoid using `double` that if I had not been forced to do for that. (I do not know if I make myself well understood, because I am also on the accuracy limit of my very imperfect English ;-) sorry! – Croises Dec 26 '14 at 00:36
3

I'd recommend rounding the number based on the number of digits in your double so that the NSDecimalNumber is truncated to only show the appropriate number of digits, thus eliminating the digits formed by potential error, ex:

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

I've adapted the countDigits: method from this answer:

- (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;
}
Community
  • 1
  • 1
Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
  • @Croises According to this high precision calculator site: http://keisan.casio.com/calculator, 56473.32 is in fact the correct answer. – Lyndsey Scott Dec 25 '14 at 16:37
  • @Croises So how is it inaccurate? That's the exact result I get from the line I provided: 56473.32 (not 56473.320000) – Lyndsey Scott Dec 25 '14 at 16:39
  • I think I am entitled to an overall accuracy of 18 digits +/- – Croises Dec 25 '14 at 16:41
  • I am looking for a method that works with 12 or 15 or the greatest – Croises Dec 25 '14 at 16:44
  • @Croises I see what you're saying. I'll type up a new recommendation... It was actually my original answer, but I thought I misunderstood you at the time... – Lyndsey Scott Dec 25 '14 at 16:53
  • @Croises Updated. (Sorry for deleting and undeleting, I had to fix that other method I linked to more than I realized...their solution wasn't quite right...) – Lyndsey Scott Dec 25 '14 at 17:09
  • @Croises I've also edited your question to make it a bit clearer, but feel free to roll it back if you don't think I've reflected your original question properly. – Lyndsey Scott Dec 25 '14 at 17:22
  • Thank you very much for the answer (and also for corrections). I think it is very close to what I wanted. I was just hoping to find a more instantaneous method of calculation for `countDigits` (based on the content of the `double` format), but I still do not know if it is possible. I'm going to experiment and I come to you in the coming days. – Croises Dec 26 '14 at 00:12
  • I did some tests. And it works perfectly with `23.5 * 45.6 * 52.7` But this is not the case with `2.35 * 45.6 * 52.7` we must change `insignificantDigit = 11` with `2.35` I do not understand why. this is certainly related to the binary representation of the result. – Croises Dec 26 '14 at 13:27
  • @Croises It worked for me... 56112.852 . What result were you expecting. – Lyndsey Scott Dec 26 '14 at 14:28
  • With `insignificantDigit = 18` ?, my result is `5647.332000000001` – Croises Dec 26 '14 at 14:32
  • Yes, with insignificantDigit 18 my result was 56112.852. How are you printing your calcDN: `NSLog(@"%@", calcDN);`? – Lyndsey Scott Dec 26 '14 at 14:35
  • @Croises Oh, just realized I'd entered your numbers incorrectly. One sec. – Lyndsey Scott Dec 26 '14 at 14:39
  • It's impossible with `2.35 * 45.6 * 52.7`... but my code is `NSLog(@"calcDN = %@", [calcDN stringValue]);` – Croises Dec 26 '14 at 14:40
  • @Croises The problem is that the `double`s precision is off, but my formula specifically truncates the `NSDecimalNumber` based on the double's digits. I was assuming that you wanted to convert the double. – Lyndsey Scott Dec 26 '14 at 15:01
  • @Croises I guess you don't really want to convert the double then as I thought... If you want accuracy, it looks like `decimalNumberByMultiplyingBy:` is the best way to go. Why did you say you can't use it? – Lyndsey Scott Dec 26 '14 at 15:05
  • @Croises Or, I'm not sure how precise you need the results to be, but you can increase the .000000001f in `(fabs(fracpart) > 0.000000001f` to .0000001f for example so it ignores any part of the fraction after 6 zeros instead of 8. – Lyndsey Scott Dec 26 '14 at 15:08
  • 1
    I can not use them (decimalNumberByMultiplyingBy:) for all, because I have many complex calculations (trigonometry, etc.). But I'll make a big effort in this direction. I think I'll stay with your solutions for now (I added +1). You allow me, I think, to leave it open for a few weeks to see if a better solution would be proposed. I have the right to dream, this is the season I've had the fairy who has looked at my problem. Thank you again! – Croises Dec 26 '14 at 15:18
  • No problem :) Yeah, if you find anything better let me know. But I'm pretty sure it won't be found using a double since the precision is off. You'll probably have to find a solution that uses NSDecimalNumber from the very beginning. – Lyndsey Scott Dec 26 '14 at 15:20
  • I think you are right. It is not possible to do otherwise. Thank you again. – Croises Dec 27 '14 at 17:44