diff --git a/Source/NSNumber.m b/Source/NSNumber.m index da05e41625..c4af015f94 100644 --- a/Source/NSNumber.m +++ b/Source/NSNumber.m @@ -40,7 +40,6 @@ # endif #endif - #import "common.h" #import "Foundation/NSCoder.h" #import "Foundation/NSDecimalNumber.h" @@ -708,7 +707,68 @@ - (BOOL) isEqualToValue: (NSValue*)aValue - (NSUInteger) hash { - return (unsigned)[self doubleValue]; + NSUInteger hash; + const char type = *[self objCType]; + + union DoubleComponents + { + double d; + uint64_t u; + }; + union FloatComponents + { + float f; + uint32_t u; + }; + + switch (type) + { + case 'd': + { + union DoubleComponents c; + + c.d = [self doubleValue]; + + // Return the unsignedIntegerValue if the floating point's fractional + // component is zero. + if (c.d == floor(c.d)) + { + return [self unsignedIntegerValue]; + } + + // Special cases. There is a positive and negative zero, make sure that + // the hashes are not different. + if (isnan(c.d) || c.d == 0.0) + { + return 0; + } + + // Return the raw bit representation of the floating point. + return c.u; + } + case 'f': + { + union FloatComponents c; + + c.f = [self floatValue]; + + if (c.f == floorf(c.f)) + { + return [self unsignedIntegerValue]; + } + + if (isnanf(c.f) || c.f == 0.0) + { + return 0; + } + + return c.u; + } + default: + hash = [self unsignedIntegerValue]; + } + + return hash; } - (NSString*) stringValue diff --git a/Tests/base/NSNumber/test01.m b/Tests/base/NSNumber/test01.m index 3130822ded..e7004d65ac 100644 --- a/Tests/base/NSNumber/test01.m +++ b/Tests/base/NSNumber/test01.m @@ -113,6 +113,60 @@ int main() PASS([zero compare: n] == NSOrderedDescending, "zero greater than -1.01") END_SET("zero checks") + + START_SET("hashing") + // Consistency - a number's hash should be the same every time. + NSNumber *n = [NSNumber numberWithInt:42]; + PASS([n hash] == [n hash], "hashing is consistent for int"); + n = [NSNumber numberWithFloat:M_PI]; + PASS([n hash] == [n hash], "hashing is consistent for float"); + n = [NSNumber numberWithDouble:M_PI]; + PASS([n hash] == [n hash], "hashing is consistent for double"); + + // Equality - equal numbers should have the same hash. + NSNumber *a = [NSNumber numberWithInt:42]; + NSNumber *b = [NSNumber numberWithInt:42]; + PASS([a hash] == [b hash], "equal int numbers have same hash"); + a = [NSNumber numberWithFloat:42.0f]; + b = [NSNumber numberWithDouble:42.0]; + PASS([a hash] == [b hash], "42.0f and 42.0dd have the same hash"); + a = [NSNumber numberWithLongLong:LLONG_MAX]; + b = [NSNumber numberWithUnsignedLongLong:LLONG_MAX]; + PASS([a hash] == [b hash], "LLONG_MAX and ULLONG_MAX-ish have same hash"); + + // Floating point numbers with zero fractional component. + a = [NSNumber numberWithDouble:42.0]; + b = [NSNumber numberWithInt:42]; + PASS([a hash] == [b hash], "double with zero fractional part hashes like int"); + a = [NSNumber numberWithFloat:123.0f]; + b = [NSNumber numberWithInt:123]; + PASS([a hash] == [b hash], "float with zero fractional part hashes like int"); + + // Special Cases - Zero and NaN. + a = [NSNumber numberWithDouble:0.0]; + PASS([a hash] == 0, "hash for 0.0 is 0"); + a = [NSNumber numberWithDouble:-0.0]; + PASS([a hash] == 0, "hash for -0.0 is 0"); + a = [NSNumber numberWithFloat:0.0f]; + PASS([a hash] == 0, "hash for 0.0f is 0"); + a = [NSDecimalNumber notANumber]; + PASS([a hash] == 0, "hash for NaN is 0"); + + // Verify different numbers have different hashes. + NSNumber *n1 = [NSNumber numberWithInt:1]; + NSNumber *n2 = [NSNumber numberWithInt:2]; + PASS([n1 hash] != [n2 hash], "different integers have different hashes"); + + NSNumber *f1 = [NSNumber numberWithFloat:1.0f]; + NSNumber *f2 = [NSNumber numberWithFloat:1.1f]; + PASS([f1 hash] != [f2 hash], "different floats have different hashes"); + + NSNumber *d1 = [NSNumber numberWithDouble:3.14159]; + NSNumber *d2 = [NSNumber numberWithDouble:3.14158]; + PASS([d1 hash] != [d2 hash], "different doubles have different hashes"); + + END_SET("hashing") + END_SET("NSNumber") return 0;