Converting a Double to an Integer for GetHashCode in Delphi
-
18-09-2019 - |
Question
Delphi 2009 added the GetHashCode function to TObject. GetHashCode returns an Integer which is used for hashing in TDictionary.
If you want an object to work well in TDictionary, you need to override GetHashCode appropriately such that, in general, different objects return different integer hash codes.
But what do you do for objects containing double fields? How do you turn those double values into a integers for GetHashCode?
The way it's usually done in Java, say, is to use a method like Double.doubleToLongBits or Float.floatToIntBits. The latter has documentation that describes it as follows: "Returns a representation of the specified floating-point value according to the IEEE 754 floating-point "single format" bit layout." This involves some bitwise operations with different masks for the different bits of a floating point value.
Is there a function that does this in Delphi?
Solution
I'd suggest the following improvement over the Gamecat code:
type
TVarRec = record
case Integer of
0: ( FInt1, FInt2 : Integer; )
1: ( FDouble : Double; )
end;
function Convert(const ADouble: Double): Integer;
var
arec : TVarRec;
begin
arec.FDouble := ADouble;
Result := arec.FInt1 xor arec.FInt2;
end;
This takes into account all the bits of the Double value.
(comments do not work well with code)
OTHER TIPS
If you want to map a double to an integer, you can use a variant record:
type
TVarRec = record
case Integer of
0: ( FInt : Integer; )
1: ( FDouble : Double; )
end;
function Convert(const ADouble: Double): Integer;
var
arec : TVarRec;
begin
arec.FDouble := ADouble;
Result := arec.FInt;
end;
Beware that this does a bitwise copy without interpretation of the values.
Another (kind of dirty trick, is using absolute variables:
function Convert(const ADouble: Double): Integer;
var
tempDouble : Double;
tempInt : Integer absolute tempDouble; // tempInt is at the same memory position as tempDouble.
begin
tempDouble := ADouble;
Result := tempInt;
end;
There's really no need to do something like this, because the default value of GetHashCode already returns a number that's guaranteed to be unique for each object: the object's memory address. Furthermore, the default hash value isn't going to change if you change the data your object contains.
Let's say you have an object that contains a Double with a value of 3.5, and you hash it and put it into a dictionary, and you get a hash code of 12345678. You also have something else holding a reference to it, and that Double field gets changed and now it's got a value of 5.21. Next time you attempt to calculate its hash value, your hash code is now 23456789, and your lookup will fail.
Unless you can guarantee that this will never happen, and you have a really good reason not to use the memory address, your best bet is to just leave GetHashCode as it is. (If it ain't broke, don't fix it.)
I guess the Java thing can be implemented in Delphi like this:
type
TVarRec = record
case Integer of
0: ( FInt1: Integer; )
1: ( FSingle: Single; )
end;
function GetHashCode(Value: Double): Integer;
var
arec: TVarRec;
begin
arec.FSingle := Value;
Result := arec.FInt1;
end;
The idea behind is to reduce the precision of the Double value to match the binary size of an Integer (Sizeof(Single) = Sizeof(Integer)). If your values can be represented in Single precision without collision, this will give a good hash value.
Edit: As the typecast won't compile in my D2009, I adapted the variant record solution.
Use CRC32 on the Double data because xor is evil.
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
TVarRec = record
case Integer of
0: ( FInt1, FInt2 : Integer; );
1: ( FDouble : Double; );
end;
function Convert(const ADouble: Double): Integer;
var
arec : TVarRec;
begin
arec.FDouble := ADouble;
Result := arec.FInt1 xor arec.FInt2;
end;
var
FDoubleVar1, FDoubleVar2: TVarRec;
HashCode1, HashCode2: Integer;
begin
// Make a Double
FDoubleVar1.FInt1 := $DEADC0DE;
FDoubleVar1.FInt2 := $0C0DEF00;
// Make another Double
FDoubleVar2.FInt1 := $0C0DEF00;
FDoubleVar2.FInt2 := $DEADC0DE;
WriteLn('1rst Double : ', FDoubleVar1.FDouble);
WriteLn('2nd Double : ', FDoubleVar2.FDouble);
HashCode1 := Convert(FDoubleVar1.FDouble);
HashCode2 := Convert(FDoubleVar2.FDouble);
WriteLn('1rst HashCode : ', HashCode1);
WriteLn('2nd HashCode : ', HashCode2);
if HashCode1 = HashCode2 then
begin
WriteLn('Warning: Same HashCode!');
end;
ReadLn;
end.