Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
2026-07-08 Todd White <todd.white@thalion.global>

* Source/NSNumber.m (GSHashDouble, -[NSNumber hash]): Derive the hash
from -doubleValue by reducing the value modulo a Mersenne prime, so
that equal numbers held in different types hash equally (as -isEqual:
requires) and fractional values no longer collide on their integral
part.
* Tests/base/NSNumber/NSNumber_hash.m: New test for the -hash /
-isEqual: contract, for fractional scattering and for non-finite
values.

2026-07-08 Todd White <todd.white@thalion.global>

* Headers/Foundation/NSString.h, Headers/Foundation/NSData.h,
Expand Down
87 changes: 86 additions & 1 deletion Source/NSNumber.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,91 @@
# include <objc/runtime.h>
#endif

#include <math.h>
#include <stdint.h>

/* An NSNumber compares equal to another whenever -compare: returns
* NSOrderedSame, and that comparison is performed on the -doubleValue of
* the two numbers (except between two integers, which are compared as
* integers but then have identical -doubleValue anyway). A conforming
* -hash must therefore be a function of -doubleValue alone; hashing the
* raw storage would give equal numbers held in different types (an int
* and a double of the same value) different hashes and break their use as
* dictionary keys.
*
* This maps the value into the range of a Mersenne prime modulus, after
* the manner of CPython's number hashing, so that the whole value
* contributes to the hash and an integral value hashes to that integer.
*/
#if UINTPTR_MAX > 0xffffffffUL
# define GS_HASH_BITS 61
#else
# define GS_HASH_BITS 31
#endif
#define GS_HASH_MODULUS (((NSUInteger)1 << GS_HASH_BITS) - 1)
#define GS_HASH_INF ((NSUInteger)314159)

static NSUInteger
GSHashDouble(double value)
{
int e, sign;
double m;
NSUInteger x, y;

if (isnan(value))
{
return 0;
}
if (isinf(value))
{
return (value > 0) ? GS_HASH_INF : (NSUInteger)(0 - GS_HASH_INF);
}

m = frexp(value, &e);
sign = 1;
if (m < 0)
{
sign = -1;
m = -m;
}

/* Consume the mantissa 28 bits at a time, accumulating modulo the
* prime. 28 is below GS_HASH_BITS for both the 32 and 64 bit case.
*/
x = 0;
while (m)
{
x = ((x << 28) & GS_HASH_MODULUS) | (x >> (GS_HASH_BITS - 28));
m *= 268435456.0; /* 2^28 */
e -= 28;
y = (NSUInteger)m; /* pull out the integer part */
m -= y;
x += y;
if (x >= GS_HASH_MODULUS)
{
x -= GS_HASH_MODULUS;
}
}

/* Fold in the exponent, reduced modulo GS_HASH_BITS. */
e = e % GS_HASH_BITS;
if (e < 0)
{
e += GS_HASH_BITS;
}
x = ((x << e) & GS_HASH_MODULUS) | (x >> (GS_HASH_BITS - e));

if (sign < 0)
{
x = 0 - x;
}
if (x == (NSUInteger)-1)
{
x = (NSUInteger)-2;
}
return x;
}

/*
* NSNumber implementation. This matches the behaviour of Apple's
* implementation. Values in the range -1 to 12 inclusive are mapped to
Expand Down Expand Up @@ -713,7 +798,7 @@ - (BOOL) isEqualToValue: (NSValue*)aValue

- (NSUInteger) hash
{
return (unsigned)[self doubleValue];
return GSHashDouble([self doubleValue]);
}

- (NSString*) stringValue
Expand Down
115 changes: 115 additions & 0 deletions Tests/base/NSNumber/NSNumber_hash.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#import "ObjectTesting.h"
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSValue.h>

#include <math.h>

int main()
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];

START_SET("equal numbers hash equally regardless of type")
/* -isEqual: on NSNumber is defined by -compare:, which promotes to
* double when the two numbers have different types, so numbers with
* the same value held as int, long long, unsigned, float or double
* all compare equal and therefore must hash equally. */
NSMutableArray *nums = [NSMutableArray array];
long long iv[] = {0, 1, -1, 42, -42, 255, 256, 65536,
2147483647LL, 2147483648LL, 4294967296LL};
unsigned i;
NSUInteger a, b, n;
BOOL ok = YES;

for (i = 0; i < sizeof(iv) / sizeof(iv[0]); i++)
{
long long v = iv[i];

[nums addObject: [NSNumber numberWithInt: (int)v]];
[nums addObject: [NSNumber numberWithLongLong: v]];
if (v >= 0)
{
[nums addObject: [NSNumber numberWithUnsignedLongLong:
(unsigned long long)v]];
}
[nums addObject: [NSNumber numberWithFloat: (float)v]];
[nums addObject: [NSNumber numberWithDouble: (double)v]];
}

n = [nums count];
for (a = 0; a < n && ok; a++)
{
for (b = 0; b < n && ok; b++)
{
NSNumber *x = [nums objectAtIndex: a];
NSNumber *y = [nums objectAtIndex: b];

if ([x isEqual: y] && [x hash] != [y hash])
{
NSLog(@"%@ isEqual: %@ but %lu != %lu", x, y,
(unsigned long)[x hash], (unsigned long)[y hash]);
ok = NO;
}
}
}
PASS(ok, "equal numbers of differing types produce equal hashes")

PASS([[NSNumber numberWithInt: 42] hash]
== [[NSNumber numberWithDouble: 42.0] hash]
&& [[NSNumber numberWithInt: 42] hash]
== [[NSNumber numberWithFloat: 42.0f] hash],
"an integer and a floating point 42 hash equally")
END_SET("equal numbers hash equally regardless of type")

START_SET("the fractional part is not discarded")
/* Distinct fractional values must hash distinctly rather than all
* folding onto their common integral part. */
NSNumber *a = [NSNumber numberWithDouble: 1.5];
NSNumber *b = [NSNumber numberWithDouble: 1.7];
NSNumber *c = [NSNumber numberWithDouble: 1.9];
unsigned distinct = 0;
unsigned i;
NSUInteger seen[100];

PASS([a hash] != [b hash] && [b hash] != [c hash] && [a hash] != [c hash],
"1.5, 1.7 and 1.9 hash distinctly")

/* A scattering sanity check: the hashes of 0.5 .. 99.5 are mostly
* distinct rather than all folding onto their integral part. */
for (i = 0; i < 100; i++)
{
NSUInteger h = [[NSNumber numberWithDouble: i + 0.5] hash];
unsigned j;
BOOL dup = NO;

for (j = 0; j < distinct; j++)
{
if (seen[j] == h)
{
dup = YES;
break;
}
}
if (!dup)
{
seen[distinct++] = h;
}
}
PASS(distinct > 95, "fractional values scatter across the hash range")
END_SET("the fractional part is not discarded")

START_SET("non-finite values are handled")
NSNumber *pinf = [NSNumber numberWithDouble: INFINITY];
NSNumber *ninf = [NSNumber numberWithDouble: -INFINITY];
NSNumber *nan = [NSNumber numberWithDouble: NAN];

PASS([pinf hash] == [[NSNumber numberWithDouble: INFINITY] hash],
"+infinity hashes consistently")
PASS([pinf hash] != [ninf hash],
"+infinity and -infinity hash distinctly")
PASS([nan hash] == [nan hash], "a NaN hash does not crash")
END_SET("non-finite values are handled")

[arp release];
return 0;
}
Loading