internal/functions/Confirm-ObjectValueEqualityDeep.ps1
function Confirm-ObjectValueEqualityDeep { [CmdletBinding()] param( [Parameter(Position = 0)] $Object1, [Parameter(Position = 1)] $Object2, [switch] $HandleRandomOrderArray, [switch] $CaseInsensitiveKeys ) if ($Object1 -eq $Object2) { # $Object1 and $Object2 are equal (includes both $null) return $true } elseif ($null -eq $Object1 -or $null -eq $Object2) { # $Object1 and $Object2 are not equal, but one of them is $null if ($null -eq $Object1) { # $Object1 is $null, swap $Object1 and $Object2 to ensure that Object1 (the old Object2) is not $null and Object2 (the old Object1) is $null (setting it to $null is ommited because it is not used in the subsequent code) $Object1 = $Object2 } if ($Object1 -is [System.Collections.IList]) { # $Object1 is an array or ArrayList, if it is empty treat it as $null and therefore equal to Object2 return $Object1.Count -eq 0 } elseif ($Object1 -is [System.Collections.IDictionary]) { # $Object1 is a hashtable, if it is empty treat it as $null and therefore equal to Object2 return $Object1.Count -eq 0 } elseif ($Object1 -is [string]) { # $Object1 is a string, if it is empty or only conatins whitespace treat it as $null and therefore equal to Object2 return [string]::IsNullOrWhiteSpace($Object1) } else { # $Object1 has something else and not null and therefore not equal to Object2 return $false } } else { $type1 = $Object1.GetType() $typeName1 = $type1.Name $type2 = $Object2.GetType() $typeName2 = $type2.Name if ($Object1 -is [System.Collections.Ilist] -or $Object2 -is [System.Collections.Ilist]) { # $Object1 or $Object2 is an array or ArrayList if ($Object1 -isnot [System.Collections.Ilist]) { $Object1 = @($Object1) } elseif ($Object2 -isnot [System.Collections.Ilist]) { $Object2 = @($Object2) } if ($Object1.Count -ne $Object2.Count) { return $false } else { # iterate and recurse if ($HandleRandomOrderArray) { $object2List = [System.Collections.ArrayList]::new($Object2) foreach ($item1 in $Object1) { # iterate through Object1 and find a match in Object2 $foundMatch = $false for ($i = 0; $i -lt $object2List.Count; $i++) { $item2 = $object2List[$i] if ($item1 -eq $item2 -or (Confirm-ObjectValueEqualityDeep $item1 $item2 -HandleRandomOrderArray -CaseInsensitiveKeys:$CaseInsensitiveKeys)) { # if either the array item values are equal or a deep inspection shows equal, continue to the next item by: # 1. Setting $foundMatch to $true # 2. Remove the matching item from the Object2 list, therefore reducing the computing complexity of the next iteration # 3. Breaking out of the inner "for" loop $foundMatch = $true $null = $object2List.RemoveAt($i) break } } if (!$foundMatch) { # no item in Object2 matches the current item in Object1, return false return $false } } # every item in Object1 has a match in Object2, return true return $object2List.Count -eq 0 } else { # iterate and recurse for ($i = 0; $i -lt $Object1.Count; $i++) { $item1 = $Object1[$i] $item2 = $Object2[$i] if ($item1 -eq $item2 -or (Confirm-ObjectValueEqualityDeep $item1 $item2 -CaseInsensitiveKeys:$CaseInsensitiveKeys)) { # if either the array item values are equal or a deep inspection shows equal, continue to the next item } else { # if the array item values are not equal and a deep inspection does not show equal, return false return $false } } # every item in the array has the same value, return true return $true } } } elseif ($typeName1 -eq "DateTime" -or $typeName2 -eq "DateTime") { # $Object1 or $Object2 is a DateTime # Note: this must be done prior to the next test, since [DateTime] is a [System.ValueType] $dateString1 = $Object1 $dateString2 = $Object2 if ($typeName1 -eq "DateTime") { $dateString1 = $Object1.ToString("yyyy-MM-dd") } if ($typeName2 -eq "DateTime") { $dateString2 = $Object2.ToString("yyyy-MM-dd") } return $dateString1 -eq $dateString2 } elseif ($typeName1 -eq "String" -or $typeName2 -eq "String" -or $Object1 -is [System.ValueType] -or $Object2 -is [System.ValueType]) { # Will have caused $true by the first "if" statement if they match # Note: this must be done prior to the next test, since [string and [System.ValueType] are both [PSCustomObject] (PSCustomObject is PowerShells version of [object]) return $false } elseif (($Object1 -is [System.Collections.IDictionary] -or $Object1 -is [PSCustomObject]) ` -and ($Object2 -is [System.Collections.IDictionary] -or $Object2 -is [PSCustomObject])) { # normalize Object1 and Object2 to hashtable $normalizedObject1 = $Object1 $normalizedObject2 = $Object2 if ($Object1 -isnot [System.Collections.IDictionary]) { $normalizedObject1 = $Object1 | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json -AsHashtable } if ($Object2 -isnot [System.Collections.IDictionary]) { $normalizedObject2 = $Object2 | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json -AsHashtable } $allKeys = $normalizedObject1.Keys + $normalizedObject2.Keys $uniqueKeys = $allKeys | Sort-Object -Unique foreach ($key in $uniqueKeys) { #recurse $item1 = $normalizedObject1.$key $item2 = $normalizedObject2.$key if ($CaseInsensitiveKeys) { if ($null -eq $item1) { # case of key does not match, find key for normalizedObject1 without considering case $key1 = $normalizedObject1.Keys | Where-Object { $_.ToLower() -eq $key.ToLower() } Write-Debug "key '$key' exists with a different case '$key1' in Object1 '$($normalizedObject1 | ConvertTo-Json -Depth 100 -Compress)'" if ($null -ne $key1) { $item1 = $normalizedObject1.$key1 } # else keep $item1 as $null; the recursive call will handle this case } if ($null -eq $item2) { # case of key does not match, find key for normalizedObject2 without considering case $key2 = $normalizedObject2.Keys | Where-Object { $_.ToLower() -eq $key.ToLower() } Write-Debug "key '$key' exists with a different case '$key2' in Object2 '$($normalizedObject2 | ConvertTo-Json -Depth 100 -Compress)'" if ($null -ne $key2) { $item2 = $normalizedObject2.$key2 } # else keep $item2 as $null; the recursive call will handle this case } } if ($item1 -eq $item2 -or (Confirm-ObjectValueEqualityDeep $item1 $item2 -CaseInsensitiveKeys:$CaseInsensitiveKeys -HandleRandomOrderArray:$HandleRandomOrderArray)) { # if the values are equal, or a deep inspection shows equal then continue to the next key } else { # if the values are not equal, and a deep inspection does not show equal, return false return $false } } # every entry in the collection has the same value, return true return $true } else { # equality of other types handled in the then clause of the outer else clause return $false } } } |