NSNumber compare: returning different results
Asked Answered
A

4

7

I'm trying to do some number comparisons and I'm getting some weird results.

NSNumber* number1 = [NSNumber numberWithFloat:1.004];
NSNumber* number2 = [NSNumber numberWithDouble:1.004];

([number1 compare:number2] == NSOrderedSame) ? NSLog(@"YES") : NSLog(@"NO");
([number1 compare:number2] == NSOrderedAscending) ? NSLog(@"YES") : NSLog(@"NO");
([number1 doubleValue] == [number2 doubleValue]) ? NSLog(@"YES") : NSLog(@"NO");
([number1 floatValue] == [number2 floatValue]) ? NSLog(@"YES") : NSLog(@"NO");

Log output:

NO
YES
NO
YES

This is extremely frustrating to me. I know this is probably because the difference between the number of bits in a float compared to a double. It seems to me it's truncating the double down to a float to do the compare. But if I don't know how the number is created, how do I get the correct results? Is there a different way to compare NSNumber's?

Antibiotic answered 9/2, 2012 at 22:33 Comment(2)
What about if you use [number1 isEqualToNumber:number2], that's the preferred way, at least if you are only trying to ascertain equality of values?Devaluation
Sorry twilson, that still returns NO. The numbers ARE different.Glaring
P
7

I tried using isEqualToNumber: and it returned NO. The reason they aren't the same is because 1.004 can't be represented exactly in binary. The double approximation has more digits after the decimal point than the float approximation so the two numbers are different. Normally, when comparing floating point numbers, you test to see if they are equal to within a tolerance value fabs(a - b) < tolerance:

NSNumber* number1 = [NSNumber numberWithFloat:1.004];
NSNumber* number2 = [NSNumber numberWithDouble:1.004];

NSLog(@"number 1: %.12f", [number1 doubleValue]);
NSLog(@"number 2: %.12f", [number2 doubleValue]);

([number1 isEqualToNumber:number2]) ? NSLog(@"YES") : NSLog(@"NO");
fabs([number1 floatValue] - [number2 doubleValue]) < 1.0e-7 ? NSLog(@"YES") : NSLog(@"NO");

results:

2012-02-09 15:08:34.272 so9219935[3313:903] number 1: 1.003999948502
2012-02-09 15:08:34.274 so9219935[3313:903] number 2: 1.004000000000
2012-02-09 15:08:34.275 so9219935[3313:903] NO
2012-02-09 15:08:34.275 so9219935[3313:903] YES
Piscatelli answered 9/2, 2012 at 23:5 Comment(4)
Very good solution! It seems crazy to me that Apple doesn't handle this with the compare: method...Antibiotic
Apple (or, more accurately, the compiler writers) can't make assumptions about the programmer's tolerance for equality. All they can do is exactly what the programmer told them to do. In this case, you were comparing two unequal values. Floating point numbers is not a trivial subject. If you have some time, take a look at What Every Computer Scientist Should Know About Floating-Point Arithmetic.Piscatelli
Thank you for the reference! I've been looking for a recommended read on this. I've known about this issue for a while, just haven't done anything about it. :)Antibiotic
This is the right solution, but I would add that it could be made more readable by bundling that simple algorithm into a -[NSNumber isEqualToNumber:withTolerance:] method in a class category.Anagram
G
2

The compare: method follows the standard C rules for type conversion. For example, if you compare an NSNumber object that has an integer value with an NSNumber object that has a floating point value, the integer value is converted to a floating-point value for comparison.

Also, initializing an NSNumber with a float, then converting it to a double loses some precision.

NSNumber* number1 = [NSNumber numberWithFloat:1.004];
NSNumber* number2 = [NSNumber numberWithDouble:1.004];
NSLog(@"number1 double: %1.16f", [number1 doubleValue]);
NSLog(@"number2 double: %1.16f", [number2 doubleValue]);
NSLog(@"number1 objCType %s", [number1 objCType]);
NSLog(@"number2 objCType %s", [number2 objCType]);

2012-02-09 15:59:49.487 testNSNumber[89283:f803] number1 double: 1.0039999485015869
2012-02-09 15:59:49.488 testNSNumber[89283:f803] number2 double: 1.0040000000000000
2012-02-09 16:21:01.655 testNSNumber[4351:f803] number1 objCType f
2012-02-09 16:21:01.656 testNSNumber[4351:f803] number2 objCType d

If you know you may have a mix of float and doubles, one solution is to compare the NSNumber floatValues like you did in the last line of your code snippet in your question.

Also, you can get the type of the data in the NSNumber with the objCType method.

Glaring answered 9/2, 2012 at 22:43 Comment(1)
I really like that objType! Thank you!Antibiotic
A
2

please try the isEqualToNumber: method to check your condition

([number1 isEqualToNumber:number2]) ? NSLog(@"YES") : NSLog(@"NO");

this stackoverflow question also gives a detailed answer

Animalism answered 9/2, 2012 at 22:56 Comment(1)
This will not work as expected and will log "NO". NSNumbers initialized with float and doubles will be different values. See my answer above. The SO question you reference is using all floats.Glaring
T
0

Another solution that worked for me was to convert them to NSStrings and do a string comparison. This was because I am working with 4 digit decimal prices and this worked well for me.

-(BOOL) hasPriceChanged{
    NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
    formatter.numberStyle = NSNumberFormatterDecimalStyle;
    formatter.maximumFractionDigits = 4;
    NSString *p = [formatter stringFromNumber:self.price];
    NSString *op = [formatter stringFromNumber:self.originalPrice];
    return ![p isEqualToString:op];
}

This got rid of the issue where I was comparing a double with 2 trailing zeros and a float with none.

I'd normally warn against string comparisons like this but as my rules were always fixed it was OK for me in this case.

Tankersley answered 9/11, 2018 at 1:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.