Skip to content

Commit

Permalink
Merge pull request #49 from CSBiology/deepHash
Browse files Browse the repository at this point in the history
DeepHash
  • Loading branch information
HLWeil authored Jan 28, 2025
2 parents 86e7a0d + 90fd64c commit 30aea47
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 5 deletions.
41 changes: 36 additions & 5 deletions src/DynamicObj/DynamicObj.fs
Original file line number Diff line number Diff line change
Expand Up @@ -388,18 +388,49 @@ type DynamicObj() =
static member (?<-) (lookup:#DynamicObj,name:string,value:'v) =
lookup.SetProperty (name,value)

member this.ReferenceEquals (other: DynamicObj) = System.Object.ReferenceEquals(this,other)

member this.StructurallyEquals (other: DynamicObj) =
this.GetHashCode() = other.GetHashCode()

override this.GetHashCode () =
this.GetProperties(true)
|> Seq.sortBy (fun pair -> pair.Key)
|> HashCodes.boxHashKeyValSeq
|> fun x -> x :?> int
HashUtils.deepHash this

override this.Equals o =
match o with
| :? DynamicObj as other ->
this.GetHashCode() = other.GetHashCode()
this.StructurallyEquals(other)
| _ -> false

and HashUtils =

static member deepHash (o:obj) =
match o with
| :? DynamicObj as o ->
o.GetProperties(true)
|> Seq.sortBy (fun pair -> pair.Key)
|> HashCodes.boxHashKeyValSeqBy HashUtils.deepHash
|> fun x -> x :?> int
| :? string as s -> DynamicObj.HashCodes.hash s
#if !FABLE_COMPILER
| :? System.Collections.IDictionary as d ->
let mutable en = d.GetEnumerator()
[
while en.MoveNext() do
let c = en.Current :?> System.Collections.DictionaryEntry
HashCodes.mergeHashes (hash c.Key) (HashUtils.deepHash c.Value)
]
|> List.reduce HashCodes.mergeHashes
#endif
| :? System.Collections.IEnumerable as e ->
let en = e.GetEnumerator()
[
while en.MoveNext() do
HashUtils.deepHash en.Current
]
|> List.reduce HashCodes.mergeHashes
| _ -> DynamicObj.HashCodes.hash o

and CopyUtils =

/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions src/DynamicObj/HashCodes.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
module DynamicObj.HashCodes

// Taken from
//https://softwareengineering.stackexchange.com/a/402543
// Which points to a no-longer existing source in FSharp Core Compiler
// But can be found in
// https://github.com/dotnet/fsharp/blob/2edab1216843f20a00a7d8f171aca52cbc35d7fd/src/Compiler/Checking/AugmentWithHashCompare.fs#L171
// Or Fables mirror
// https://github.com/fable-compiler/Fable/blob/b0e640763fd90bd084f72531cb119d49a91ec077/src/fcs-fable/src/Compiler/Checking/AugmentWithHashCompare.fs#L171
let mergeHashes (hash1 : int) (hash2 : int) : int =
0x9e3779b9 + hash2 + (hash1 <<< 6) + (hash1 >>> 2)

Expand Down Expand Up @@ -46,4 +53,12 @@ let boxHashKeyValSeq (a: seq<System.Collections.Generic.KeyValuePair<'a,'b>>) :
|> Seq.fold (fun acc o ->
mergeHashes (hash o.Key) (hash o.Value)
|> mergeHashes acc) 0
|> box

let boxHashKeyValSeqBy (f : 'b -> int) (a: seq<System.Collections.Generic.KeyValuePair<'a,'b>>) : obj =
a
// from https://stackoverflow.com/a/53507559
|> Seq.fold (fun acc o ->
mergeHashes (hash o.Key) (f o.Value)
|> mergeHashes acc) 0
|> box
1 change: 1 addition & 0 deletions tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<Compile Include="DynamicObj\DeepCopyProperties.fs" />
<Compile Include="DynamicObj\Main.fs" />
<Compile Include="ReflectionUtils.fs" />
<Compile Include="HashUtils.fs" />
<Compile Include="Inheritance.fs" />
<Compile Include="Interface.fs" />
<Compile Include="DynObj.fs" />
Expand Down
275 changes: 275 additions & 0 deletions tests/DynamicObject.Tests/HashUtils.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
module HashUtils.Tests

open System
open Fable.Pyxpecto
open DynamicObj
open Fable.Core

let int1 = box 1
let int2 = box 2
let int3 = box 3
let int4 = box 4

let intDict1 =
let d = System.Collections.Generic.Dictionary<obj, obj>()
d.Add(int1, int2)
d.Add(int3, int4)
d

let intDict1' =
let d = System.Collections.Generic.Dictionary<obj, obj>()
d.Add(int1, int2)
d.Add(int3, int4)
d

let intDict2 =
let d = System.Collections.Generic.Dictionary<obj, obj>()
d.Add(int1, int4)
d.Add(int3, int2)
d

let intDict3 =
let d = System.Collections.Generic.Dictionary<obj, obj>()
d.Add(int2, int1)
d.Add(int4, int3)
d

let intDict4 =
let d = System.Collections.Generic.Dictionary<obj, obj>()
d.Add(int1, int3)
d.Add(int2, int4)
d

let intList1 = [int1;int2;int3;int4]
let intList1' = [int1;int2;int3;int4]
let intList2 = [int1;int4;int3;int2]

let nestedList1 = [intList1;intList2]
let nestedList1' = [intList1';intList2]
let nestedList2 = [intList2;intList1]

let intArray1 = [|int1;int2;int3;int4|]
let intArray1' = [|int1;int2;int3;int4|]
let intArray2 = [|int1;int4;int3;int2|]

let intSeq1 = seq { yield int1; yield int2; yield int3; yield int4 }
let intSeq1' = seq { yield int1; yield int2; yield int3; yield int4 }
let intSeq2 = seq { yield int1; yield int4; yield int3; yield int2 }

let resizeArray1 = ResizeArray [int1;int2;int3;int4]
let resizeArray1' = ResizeArray [int1;int2;int3;int4]
let resizeArray2 = ResizeArray [int1;int4;int3;int2]


let dynamicObjectWithInt1 =

let d = DynamicObj()
d.SetProperty("a", int1)
d.SetProperty("b", int2)
d

let dynamicObjectWithInt1DiffKey =
let d = DynamicObj()
d.SetProperty("a", int1)
d.SetProperty("c", int2)
d

let dynamicObjectWithInt1' =
let d = DynamicObj()
d.SetProperty("a", int1)
d.SetProperty("b", int2)
d

let dynamicObjectWithInt2 =
let d = DynamicObj()
d.SetProperty("a", int2)
d.SetProperty("b", int1)
d

let dynamicObjectWithDict1 =
let d = DynamicObj()
d.SetProperty("a", intDict1)
d.SetProperty("b", intDict2)
d

let dynamicObjectWithDict1' =
let d = DynamicObj()
d.SetProperty("a", intDict1)
d.SetProperty("b", intDict2)
d

let dynamicObjectWithDict2 =
let d = DynamicObj()
d.SetProperty("a", intDict2)
d.SetProperty("b", intDict1)
d

let dynamicObjectWithDynamicObject1 =
let d = DynamicObj()
d.SetProperty("a", dynamicObjectWithInt1)
d.SetProperty("b", dynamicObjectWithInt2)
d

let dynamicObjectWithDynamicObject1' =
let d = DynamicObj()
d.SetProperty("a", dynamicObjectWithInt1)
d.SetProperty("b", dynamicObjectWithInt2)
d

let dynamicObjectWithDynamicObject2 =
let d = DynamicObj()
d.SetProperty("a", dynamicObjectWithInt2)
d.SetProperty("b", dynamicObjectWithDict1)
d


let tests_Dictionary =
testList "Dictionary" [
testList "Shuffled Int" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash intDict1) (HashUtils.deepHash intDict1) "Same Dictionary should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash intDict1) (HashUtils.deepHash intDict1') "Structurally equal Dictionary should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intDict1) (HashUtils.deepHash intDict2) "Different Dictionary should return different Hash (1vs2)"
testCase "1v3" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intDict1) (HashUtils.deepHash intDict3) "Different Dictionary should return different Hash (1vs3)"
testCase "1v4" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intDict1) (HashUtils.deepHash intDict4) "Different Dictionary should return different Hash (1vs4)"
testCase "2v3" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intDict2) (HashUtils.deepHash intDict3) "Different Dictionary should return different Hash (2vs3)"
testCase "2v4" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intDict2) (HashUtils.deepHash intDict4) "Different Dictionary should return different Hash (2vs4)"

]
]

let tests_Lists =
testList "Lists" [
testList "Shuffled Int" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash intList1) (HashUtils.deepHash intList1) "Same List should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash intList1) (HashUtils.deepHash intList1') "Structurally equal List should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intList1) (HashUtils.deepHash intList2) "Different List should return different Hash"
]
testList "Shuffled Nested" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash nestedList1) (HashUtils.deepHash nestedList1) "Same Nested List should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash nestedList1) (HashUtils.deepHash nestedList1') "Structurally equal Nested List should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash nestedList1) (HashUtils.deepHash nestedList2) "Different Nested List should return different Hash"

]
]

let tests_Array =
testList "Array" [
testList "Shuffled Int" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash intArray1) (HashUtils.deepHash intArray1) "Same Array should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash intArray1) (HashUtils.deepHash intArray1') "Structurally equal Array should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intArray1) (HashUtils.deepHash intArray2) "Different Array should return different Hash"
]
]

let tests_Seq =
testList "Seq" [
testList "Shuffled Int" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash intSeq1) (HashUtils.deepHash intSeq1) "Same Seq should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash intSeq1) (HashUtils.deepHash intSeq1') "Structurally equal Seq should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intSeq1) (HashUtils.deepHash intSeq2) "Different Seq should return different Hash"
]
]

let tests_ResizeArray =
testList "ResizeArray" [
testList "Shuffled Int" [
testCase "1v1" <| fun _ ->

Expect.equal (HashUtils.deepHash resizeArray1) (HashUtils.deepHash resizeArray1) "Same ResizeArray should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash resizeArray1) (HashUtils.deepHash resizeArray1') "Structurally equal ResizeArray should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash resizeArray1) (HashUtils.deepHash resizeArray2) "Different ResizeArray should return different Hash"
]
]


let tests_DynamicObject =
testList "DynamicObj" [
testList "Shuffled Int" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash dynamicObjectWithInt1) (HashUtils.deepHash dynamicObjectWithInt1) "Same DynamicObject should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash dynamicObjectWithInt1) (HashUtils.deepHash dynamicObjectWithInt1') "Structurally equal DynamicObject should return consistent Hash"
testCase "1v1DiffKey" <| fun _ ->
Expect.notEqual (HashUtils.deepHash dynamicObjectWithInt1) (HashUtils.deepHash dynamicObjectWithInt1DiffKey) "Different DynamicObject should return different Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash dynamicObjectWithInt1) (HashUtils.deepHash dynamicObjectWithInt2) "Different DynamicObject should return different Hash"
]
testList "Shuffled Dict" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash dynamicObjectWithDict1) (HashUtils.deepHash dynamicObjectWithDict1) "Same DynamicObject should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash dynamicObjectWithDict1) (HashUtils.deepHash dynamicObjectWithDict1') "Structurally equal DynamicObject should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash dynamicObjectWithDict1) (HashUtils.deepHash dynamicObjectWithDict2) "Different DynamicObject should return different Hash"
]
testList "Shuffled DynamicObject" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash dynamicObjectWithDynamicObject1) (HashUtils.deepHash dynamicObjectWithDynamicObject1) "Same DynamicObject should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash dynamicObjectWithDynamicObject1) (HashUtils.deepHash dynamicObjectWithDynamicObject1') "Structurally equal DynamicObject should return consistent Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash dynamicObjectWithDynamicObject1) (HashUtils.deepHash dynamicObjectWithDynamicObject2) "Different DynamicObject should return different Hash"
]
testList "Shuffled Int AsOption" [
testCase "1v1" <| fun _ ->
Expect.equal (HashUtils.deepHash (Some dynamicObjectWithInt1)) (HashUtils.deepHash (Some dynamicObjectWithInt1)) "Same DynamicObject should return consistent Hash"
testCase "1v1'" <| fun _ ->
Expect.equal (HashUtils.deepHash (Some dynamicObjectWithInt1)) (HashUtils.deepHash (Some dynamicObjectWithInt1')) "Structurally equal DynamicObject should return consistent Hash"
testCase "1v1DiffKey" <| fun _ ->
Expect.notEqual (HashUtils.deepHash (Some dynamicObjectWithInt1)) (HashUtils.deepHash (Some dynamicObjectWithInt1DiffKey)) "Different DynamicObject should return different Hash"
testCase "1v2" <| fun _ ->
Expect.notEqual (HashUtils.deepHash (Some dynamicObjectWithInt1)) (HashUtils.deepHash (Some dynamicObjectWithInt2)) "Different DynamicObject should return different Hash"
testCase "1 v None" <| fun _ ->
Expect.notEqual (HashUtils.deepHash (Some dynamicObjectWithInt1)) (HashUtils.deepHash None) "Different DynamicObject should return different Hash"

]
testList "Mixed" [
testCase "Int vs Dict" <| fun _ ->
Expect.notEqual (HashUtils.deepHash dynamicObjectWithInt1) (HashUtils.deepHash dynamicObjectWithDict1) "Int vs Dict with same values should return different Hash"
testCase "Dict vs DynObj" <| fun _ ->
Expect.notEqual (HashUtils.deepHash dynamicObjectWithDict1) (HashUtils.deepHash dynamicObjectWithDynamicObject1) "Dict vs DynObj with same values should return different Hash"
]
]


let tests_Mixed =
testList "Mixed" [
testCase "Int vs Dict" <| fun _ ->
Expect.notEqual (HashUtils.deepHash int1) (HashUtils.deepHash intDict1) "Int vs Dict with same values should return different Hash"
testCase "List vs Dict" <| fun _ ->
Expect.notEqual (HashUtils.deepHash intList1) (HashUtils.deepHash intDict1) "List vs Dict with same values should return different Hash"
]




let main = testList "DeepHash" [
tests_Dictionary
tests_Lists
tests_Array
tests_Seq
tests_ResizeArray
tests_DynamicObject
tests_Mixed
]
1 change: 1 addition & 0 deletions tests/DynamicObject.Tests/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open Fable.Pyxpecto

let all = testSequenced <| testList "DynamicObj" [
ReflectionUtils.Tests.main
HashUtils.Tests.main
CopyUtils.Tests.main
DynamicObj.Tests.main
DynObj.Tests.main
Expand Down

0 comments on commit 30aea47

Please sign in to comment.