Search code examples
iosobjective-cdoublensdecimalnumber

Converting double to NSDecimalNumber while maintaining accuracy


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?


Solution

  • 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;
    }