Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DeepHash #49

Merged
merged 4 commits into from
Jan 28, 2025
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
41 changes: 36 additions & 5 deletions src/DynamicObj/DynamicObj.fs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
| Some pi -> Some pi
| None -> this.TryGetDynamicPropertyHelper propertyName

/// <summary>

Check warning on line 78 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

This XML comment is incomplete: no documentation for parameter 'propertyName'

Check warning on line 78 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (windows-latest)

This XML comment is incomplete: no documentation for parameter 'propertyName'
/// Returns Some(boxed property value) if a dynamic (or static) property with the given name exists, otherwise None.
/// </summary>
/// <param name="name">the name of the property to get</param>
Expand Down Expand Up @@ -388,18 +388,49 @@
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
Loading