Last active
January 10, 2023 07:43
-
-
Save swbbl/ad832a75c71fd57242e9a0e90d9ec4b6 to your computer and use it in GitHub Desktop.
Join-Object combines two collections based on property names or scriptblocks like a SQL-variant of Group-Object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using namespace System.Collections.Generic | |
function Join-Object { | |
<# | |
.SYNOPSIS | |
Join-Object combines two collections based on property names or scriptblocks like a SQL-variant of Group-Object. | |
.DESCRIPTION | |
Join-Object combines two collections based on property names or scriptblocks like a SQL-variant of Group-Object. | |
.EXAMPLE | |
$leftObjects = Get-ChildItem -LiteralPath 'C:\Windows' | |
$rightObjects = Get-ChildItem -LiteralPath 'C:\Windows.old\Windows' | |
Join-Object -LeftObject $leftObjects -RightObject $rightObjects -LeftProperty 'Name' | |
.EXAMPLE | |
$leftObjects = Get-ChildItem -LiteralPath 'C:\Windows' -Recurse -Force | |
$rightObjects = Get-ChildItem -LiteralPath 'C:\Windows.old\Windows' -Recurse -Force | |
$test = Join-Object -LeftObject $leftObjects -RightObject $rightObjects -On { $_.FullName -replace '^C:\\Windows(\.old\\Windows)?' } | |
#> | |
[CmdletBinding()] | |
param( | |
# Collection A. | |
[Parameter(Mandatory, ValueFromPipeline)] | |
[psobject[]] | |
$LeftObject, | |
# String or ScriptBlock. | |
[Parameter(Mandatory)] | |
[Alias('Property', 'On')] | |
[ValidateNotNullOrEmpty()] | |
[psobject] | |
$LeftProperty, | |
# Collection B. | |
[Parameter()] | |
[psobject[]] | |
$RightObject, | |
# String or ScriptBlock (like: { $_.PropertyName -replace '[0-9]' } ). | |
# If empty, LeftProperty will be used. | |
[Parameter()] | |
[ValidateNotNullOrEmpty()] | |
[psobject] | |
$RightProperty, | |
# SQL Join-Type. Default: FullJoin | |
# NoJoin will only return objects where the relevant property (Right/LeftProperty) is $null. | |
[Parameter()] | |
[ValidateSet('LeftJoin', 'RightJoin', 'InnerJoin', 'FullJoin', 'NoJoin')] | |
[string] | |
$JoinType = 'FullJoin', | |
# Case-sensitive join. | |
[Parameter()] | |
[switch] | |
$CaseSensitive, | |
# Returns an overview and keeps left/right objects still separated (i.e. properties are not merged). | |
[Parameter()] | |
[switch] | |
$NoMerge | |
) | |
begin { | |
$_joinTypes = if ($JoinType -ne 'FullJoin') { | |
$JoinType | |
if ($JoinType -in 'LeftJoin', 'RightJoin') { | |
'InnerJoin' | |
} | |
} | |
if ([string]::IsNullOrEmpty($RightProperty)) { | |
$RightProperty = $LeftProperty | |
} | |
$rightObjectSet = $RightObject | |
if (-not $PSCmdlet.MyInvocation.ExpectingInput) { | |
$leftObjectSet = $LeftObject | |
} else { | |
$leftObjectSet = [System.Collections.Generic.List[psobject]]::new() | |
} | |
$joinSet = if ($CaseSensitive) { | |
[Dictionary[string, Dictionary[string, List[psobject]]]]::new() | |
} else { | |
[Dictionary[string, Dictionary[string, List[psobject]]]]::new([System.StringComparer]::OrdinalIgnoreCase) | |
} | |
} | |
process { | |
if ($PSCmdlet.MyInvocation.ExpectingInput) { | |
$leftObjectSet.Add($LeftObject) | |
} | |
} | |
end { | |
foreach ($item in $leftObjectSet) { | |
# we use ForEach-method to allow Strings and ScriptBlocks as Left/RightProperty | |
$itemProperty = [string] $item.ForEach($LeftProperty) | |
if (-not $joinSet.ContainsKey($itemProperty)) { | |
$joinSet[$itemProperty] = [Dictionary[string, psobject]]::new() | |
} | |
if (-not $joinSet[$itemProperty].ContainsKey('Left')) { | |
$joinSet[$itemProperty]['Left'] = [System.Collections.Generic.List[psobject]]::new() | |
} | |
$joinSet[$itemProperty]['Left'].Add($item) | |
} | |
foreach ($item in $rightObjectSet) { | |
# we use ForEach-method to allow Strings and ScriptBlocks as Left/RightProperty | |
$itemProperty = [string] $item.ForEach($RightProperty) | |
if (-not $joinSet.ContainsKey($itemProperty)) { | |
$joinSet[$itemProperty] = [Dictionary[string, psobject]]::new() | |
} | |
if (-not $joinSet[$itemProperty].ContainsKey('Right')) { | |
$joinSet[$itemProperty]['Right'] = [System.Collections.Generic.List[psobject]]::new() | |
} | |
$joinSet[$itemProperty]['Right'].Add($item) | |
} | |
foreach ($entry in $joinSet.GetEnumerator()) { | |
$joinObjectKey = $entry.Key | |
$joinObjectLeft = $entry.Value['Left'] | |
$joinObjectRight = $entry.Value['Right'] | |
$joinObjectType = if ([string]::IsNullOrEmpty($joinObjectKey)) { | |
'NoJoin' | |
} elseif ($joinObjectLeft.Count -and -not $joinObjectRight.Count) { | |
'LeftJoin' | |
} elseif ($joinObjectRight.Count -and -not $joinObjectLeft.Count) { | |
'RightJoin' | |
} else { | |
'InnerJoin' | |
} | |
if ($null -eq $_joinTypes -or $joinObjectType -in $_joinTypes) { | |
if ($NoMerge) { | |
[PSCustomObject]@{ | |
PSTypeName = 'JoinObject' | |
Key = $joinObjectKey | |
Type = $joinObjectType | |
Count = $joinObjectLeft.Count + $joinObjectRight.Count | |
Left = $joinObjectLeft | |
Right = $joinObjectRight | |
} | |
} else { | |
$mergedObjectProps = [ordered]@{} | |
$propIsList = [HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) | |
foreach ($object in @($joinObjectLeft; $joinObjectRight)) { | |
foreach ($prop in $object.psobject.properties) { | |
$propName = $prop.Name | |
$propValue = $prop.Value | |
if ($mergedObjectProps.Contains($propName)) { | |
if ($mergedObjectProps[$propName] -notcontains $propValue) { | |
if (-not $propIsList.Contains($propName)) { | |
$currentValue = $mergedObjectProps[$propName] | |
# we use queue<T> rather than list<T>, so it's possible (in most cases) to validate which value comes from left and which from right (FIFO) | |
$mergedObjectProps[$propName] = [Queue[psobject]]::new() | |
$mergedObjectProps[$propName].Enqueue($currentValue) | |
[void] $propIsList.Add($propName) | |
} | |
$mergedObjectProps[$propName].Enqueue($propValue) | |
} | |
} else { | |
$mergedObjectProps[$propName] = $propValue | |
} | |
} | |
} | |
foreach ($propName in $propIsList) { | |
# convert queue<T> to array (e.g. to allow indexing). Order (FIFO) remains. | |
$mergedObjectProps[$propName] = $mergedObjectProps[$propName].ToArray() | |
# adding the hint that the property values are merged. | |
# Can be validated via: "if ($<object>.<property>.pstypenames.Contains('Merged')){ <doIt> }" | |
$mergedObjectProps[$propName].pstypenames.Insert(0, 'Merged') | |
} | |
[pscustomobject] $mergedObjectProps | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment