├── Join-Object.png ├── LICENSE ├── Tests └── Performance.Tests.ps1 ├── JoinModule.psd1 ├── ChangeLog.md ├── README.md └── Join.psm1 /Join-Object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iRon7/Join-Object/HEAD/Join-Object.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 iRon7 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/Performance.Tests.ps1: -------------------------------------------------------------------------------- 1 | Write-Host 'Preparing...' 2 | . .\Join-Object.ps1 3 | $Null = Random -SetSeed 0 4 | $Max = 1000 5 | $NumberFormat = '{0:' + '0' * ($Max - 1).ToString().Length + '}' 6 | 7 | $Left = foreach ($i in 0..$Max) { 8 | [pscustomobject]@{ 9 | Number = $i 10 | Random = "Random$NumberFormat" -f (Random $Max) 11 | Side = "Left$NumberFormat" -f $i 12 | } 13 | } 14 | 15 | $Right = foreach ($i in 0..$Max) { 16 | [pscustomobject]@{ 17 | Name = "Name$NumberFormat" -f $i 18 | Random = "Random$NumberFormat" -f (Random $Max) 19 | Side = "Right$NumberFormat" -f $i 20 | } 21 | } 22 | 23 | 24 | Write-Host 'Measuring inner join...' 25 | $InnerJoin = Measure-Command { 26 | $Test = $Left | Join $Right -On Random 27 | } 28 | Write-Host ($Test | Select-Object -First 5 | Format-Table | Out-String) 29 | Write-Host $InnerJoin.TotalSeconds 30 | 31 | Write-Host 'Measuring full join...' 32 | $FullJoin = Measure-Command { 33 | $Test = $Left | FullJoin $Right -On Random 34 | } 35 | Write-Host ($Test | Select-Object -First 5 | Format-Table | Out-String) 36 | Write-Host $FullJoin.TotalSeconds 37 | 38 | Write-Host 'Measuring side-by-side join...' 39 | $SideJoin = Measure-Command { 40 | $Test = $Left | FullJoin $Right 41 | } 42 | Write-Host ($Test | Select-Object -First 5 | Format-Table | Out-String) 43 | Write-Host $SideJoin.TotalSeconds 44 | 45 | Write-Host 'Total:' ($InnerJoin.TotalSeconds + $FullJoin.TotalSeconds + $SideJoin.TotalSeconds) 46 | -------------------------------------------------------------------------------- /JoinModule.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'JoinModule' 3 | # 4 | # Generated by: Ronald Bode (iRon) 5 | # 6 | # Generated on: 6/10/2023 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Join.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '3.8.4' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'de7b3716-0793-485d-b32a-eb54dee49d23' 22 | 23 | # Author of this module 24 | Author = 'Ronald Bode (iRon)' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'PowerSnippets.com' 28 | 29 | # Copyright statement for this module 30 | Copyright = 'Ronald Bode (iRon)' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Join-Object combines two object lists based on a related property between them.' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '3.0' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | # FunctionsToExport = @() 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | # AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = 'Join-Object','Join','InnerJoin','LeftJoin','RightJoin','FullJoin','OuterJoin','CrossJoin','Update','Merge','Difference','Combine','Table' 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/iRon7/Join-Object/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/iRon7/Join-Object' 105 | 106 | # A URL to an icon representing this module. 107 | IconUri = 'https://raw.githubusercontent.com/iRon7/Join-Object/master/Join-Object.png' 108 | 109 | # ReleaseNotes of this module 110 | # ReleaseNotes = '' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 116 | # RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | # ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | # HelpInfoURI = '' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## 2025-06-10 3.8.4 (iRon) 2 | - Breaking changes 3 | - [#55](https://github.com/iRon7/Join-Object/issues/55) (Bucket 3: Unlikely Grey Area) Some unapproved verb cmdlets (as e.g. `FullJoin-Object`) are now provided as aliases 4 | - Fixes 5 | - [#45](https://github.com/iRon7/Join-Object/issues/45) incorrect automatically named FullJoin -on -eq 6 | - [#46](https://github.com/iRon7/Join-Object/issues/46) #43 should exclude ScriptBlocks 7 | - [#48](https://github.com/iRon7/Join-Object/issues/48) Use AST rather than -Split to create proxy commands 8 | - [#52](https://github.com/iRon7/Join-Object/issues/52) cross join with empty right table causes error 9 | - [#55](https://github.com/iRon7/Join-Object/issues/55) Resolved "Unapproved Verb" warning 10 | - Reduced number of ParameterSets 11 | 12 | ## 2023-06-10 3.8.3 (iRon) 13 | - Fixes 14 | - [#43](https://github.com/iRon7/Join-Object/issues/43): An outer join on an empty pipeline should return the right object 15 | - [#44](https://github.com/iRon7/Join-Object/issues/44): Broken comment line help 16 | ## 2023-05-24 3.8.1 (iRon) 17 | - Fixes 18 | - [#42](https://github.com/iRon7/Join-Object/issues/42): An outer join on an empty pipeline should return the right object 19 | ## 2023-05-12 3.8.0 (iRon) 20 | - Updated 21 | - [#39](https://github.com/iRon7/Join-Object/issues/39): Improved performance (by more than a factor 2) 22 | - [#38](https://github.com/iRon7/Join-Object/issues/38): Change default `-ValueName` ([bucket 2](https://github.com/PowerShell/PowerShell/blob/master/docs/dev-process/breaking-change-contract.md#bucket-2-unlikely-grey-area) break-change) 23 | - [#37](https://github.com/iRon7/Join-Object/issues/37): Exclude identical objects on a self join where the `-equal` parameter is omitted ([bucket 3](https://github.com/PowerShell/PowerShell/blob/master/docs/dev-process/breaking-change-contract.md#bucket-3-unlikely-grey-area) break-change) 24 | - [#41](https://github.com/iRon7/Join-Object/issues/41): Improved comparison with collection values ( `@{a=1} -ne @{a=2}` ) ([bucket 3](https://github.com/PowerShell/PowerShell/blob/master/docs/dev-process/breaking-change-contract.md#bucket-3-unlikely-grey-area) break-change) 25 | - [#40](https://github.com/iRon7/Join-Object/issues/40): Improved the way multiple properties are compared 26 | - Changed comment based help to make use of the [Get-MarkdownHelp](https://github.com/iRon7/Get-MarkdownHelp) features 27 | ## 2022-04-26 3.7.1 (iRon) 28 | - New feature 29 | - Added [#30](https://github.com/iRon7/Join-Object/issues/30): Symmetric difference (OuterJoin) 30 | ## 2021-12-17 3.6.0 (iRon) 31 | - Updated 32 | - Implemented [#29](https://github.com/iRon7/Join-Object/issues/29): key expressions (requires explicit `-Using` parameter for compare expressions) 33 | ## 2021-07-27 3.5.4 (iRon) 34 | - Fixes 35 | - Fixed issue [#28](https://github.com/iRon7/Join-Object/issues/28): FullJoin doesn't work properly when joining multiple array when one of the array is empty 36 | ## 2021-07-06 3.5.3 (iRon) 37 | - Fixes 38 | - Fixed issue [#27](https://github.com/iRon7/Join-Object/issues/27): MissingLeftProperty: `Join-Object` : The property 'xxx' cannot be found on the left object. 39 | ## 2021-06-14 3.5.2 (iRon) 40 | - Help 41 | - Minor Help update and advertisement for the Module version. 42 | ## 2021-06-10 3.5.1 (iRon) 43 | - Fixes 44 | - Fixed ScriptBlock module scope issue: https://stackoverflow.com/q/2193410/1701026 45 | ## 2021-06-08 3.5.0 (iRon) 46 | - Updated 47 | - Prepared for module version 48 | ## 2021-04-08 3.4.7 (iRon) 49 | - Updated 50 | - Improved proxy command defaults 51 | ## 2021-04-08 3.4.6 (iRon) 52 | - Fixes 53 | - Fixed issue [#19](https://github.com/iRon7/Join-Object/issues/19): Deal with empty (and `$Null`) inputs 54 | ## 2021-03-11 3.4.5 (iRon) 55 | - Help 56 | - Issue [#17](https://github.com/iRon7/Join-Object/issues/17) Updated help 57 | ## 2021-03-24 3.4.4 (iRon) 58 | - Updated 59 | - Using `$PSCmdlet`.ThrowTerminatingError for argument exceptions 60 | ## 2021-03-20 3.4.2 (iRon) 61 | - Updated 62 | - Issue [#18](https://github.com/iRon7/Join-Object/issues/18) Support self-join on the left (piped) object 63 | ## 2021-03-11 3.4.2 (iRon) 64 | - Help 65 | - Code and Help clearance 66 | ## 2021-03-08 3.4.1 (iRon) 67 | - Updated 68 | - Implemented issue [#16](https://github.com/iRon7/Join-Object/issues/16) "Discern merged properties for multiple joins" 69 | ## 2021-03-01 3.4.0 (iRon) 70 | - Updated 71 | - Implemented issue [#14](https://github.com/iRon7/Join-Object/issues/14) "Support non-object arrays" 72 | ## 2020-08-09 3.3.0 (iRon) 73 | - Updated 74 | - Convert each object to a hash table for strict expressions 75 | - Support wildcard * (all properties) for the `-On` parameter 76 | - Prevent against code injection: https://devblogs.microsoft.com/powershell/powershell-injection-hunter-security-auditing-for-powershell-scripts/ 77 | - Implemented smarter properties merge: [#12](https://github.com/iRon7/Join-Object/issues/12) 78 | - Reformatted script with https://github.com/DTW-DanWard/PowerShell-Beautifier 79 | ## 2020-04-05 3.2.2 (iRon) 80 | - Updated 81 | - Better handling argument exceptions 82 | ## 2020-01-19 3.2.1 (iRon) 83 | - Updated 84 | - Issue [#10](https://github.com/iRon7/Join-Object/issues/10): Support for dictionaries (hashtable, ordered, ...) 85 | ## 2019-12-16 3.2.0 (iRon) 86 | - Updated 87 | - Defined stricter parameter sets (separated `-On` and `-OnExpression` ) 88 | ## 2019-12-10 3.1.6 (iRon) 89 | - New feature 90 | - Added `-MatchCase` (alias `-CaseSensitive`) parameter 91 | ## 2019-12-09 3.1.5 (iRon) 92 | - New feature 93 | - Added `-Strict` parameter 94 | ## 2019-12-02 3.1.4 (iRon) 95 | - Updated 96 | - Throw "The `-On` parameter cannot be used on a cross join." 97 | ## 2019-11-15 3.1.3 (iRon) 98 | - Updated 99 | - Also apply `-Where` argument to outer join part (expression to evaluate `$Null` values) 100 | ## 2019-11-11 3.1.2 (iRon) 101 | - Fixes 102 | - Resolved bug with single right object 103 | ## 2019-11-10 3.1.1 (iRon) 104 | - Updated 105 | - Improved `-Property` * implementation 106 | ## 2019-11-08 3.1.0 (iRon) 107 | - Help 108 | - Adjusted help 109 | ## 2019-11-07 3.0.8 (iRon) 110 | - Updated 111 | - All properties of the `$Left` and `$Right` object are set to `$Null` in the outer join part. 112 | - Better support chaining multiple joins and simplified available expression objects: 113 | ## 2019-11-01 3.0.7 (iRon) 114 | - Updated 115 | - Renamed `-Unify` parameter to `-Discern` and divided `-Discern` from `-Property` parameter 116 | ## 2019-07-16 3.0.6 (iRon) 117 | - Updated 118 | - Issue [#6](https://github.com/iRon7/Join-Object/issues/6), improved performance (~2x on large tables), thanks to @burkasaurusrex' suggestion 119 | ## 2019-07-14 3.0.5 (meany) 120 | - Fixes 121 | - Issue [#5](https://github.com/iRon7/Join-Object/issues/5), resolved: Cannot dot source / invoke script on 2012 R2 bug 122 | ## 2019-07-03 3.0.4 (iRon) 123 | - Updated 124 | - Experimental version (not implemented) 125 | ## 2019-07-02 3.0.3 (iRon) 126 | - Updated 127 | - Support for datatables 128 | ## 2019-04-10 3.0.2 (iRon) 129 | - Fixes 130 | - Fixed default unify issue due to `-On` case difference 131 | ## 2019-03-30 3.0.1 (iRon) 132 | - Updated 133 | - Updated embedded examples 134 | ## 2019-03-30 3.0.0 (iRon) 135 | - New feature 136 | - New release with new test set 137 | ## 2019-03-29 3.7.1 (iRon) 138 | - Updated 139 | - Improved self join syntax 140 | ## 2019-03-25 3.7.0 (iRon) 141 | - New feature 142 | - Added `-Where` clause 143 | ## 2019-03-10 2.6.0 (iRon) 144 | - Updated 145 | - Improved performance by using a HashTable for the inner (right) loop where possible 146 | ## 2019-03-04 2.5.2 (iRon) 147 | - Updated 148 | - Changed `-Pair` to `-Unify` 149 | ## 2019-03-03 2.5.1 (iRon) 150 | - New feature 151 | - Added `-Pair` (alias `-Merge`) feature to separate duplicated unrelated property names 152 | ## 2019-02-24 2.4.4 (iRon) 153 | - Fixes 154 | - Resolved scope bug when invoked multiple times in the same stream 155 | ## 2019-02-06 2.4.3 (iRon) 156 | - Updated 157 | - Changed `$LeftOrNull` and `$RightOrNull` to `$LeftOrVoid` and `$RightOrVoid` 158 | ## 2019-02-08 2.4.2 (mcclimont) 159 | - Updated 160 | - Compliant with StrictMode `-Version` 2 (https://github.com/iRon7/Join-Object/pull/3) 161 | ## 2019-02-06 2.4.1 (iRon) 162 | - New feature 163 | - Added `$LeftOrRight` and `$RightOrLeft` references 164 | ## 2019-02-02 2.4.0 (iRon) 165 | - New feature 166 | - Added Update-Object and Merge-Object proxy commands 167 | ## 2019-02-01 2.3.2 (iRon) 168 | - Updated 169 | - The `-MergeExpression` is only used in case the Left and Right properties overlap 170 | ## 2018-12-30 2.3.1 (iRon) 171 | - New feature 172 | - Added CrossJoin Type. If the `-On` parameter is omitted, a join by index will be done 173 | ## 2018-11-28 2.3.0 (iRon) 174 | - Updated 175 | - Replaced `InnerJoin-`, `LeftJoin-`, `RightJoin-`, `FullJoin-Object` aliases by proxy commands 176 | ## 2018-11-28 2.2.6 (iRon) 177 | - Updated 178 | - Support for mixed `[string]Key`/`[hashtable]@{Key={Expression}}` `-Property` parameter 179 | ## 2018-11-27 2.2.5 (iRon) 180 | - Fixes 181 | - Fixed empty output bug (including test) 182 | ## 2018-03-25 2.2.4 (iRon) 183 | - Updated 184 | - Keeping the properties in order 185 | ## 2018-03-25 2.2.3 (iRon) 186 | - Updated 187 | - Supply a list of properties by: `-Property` [String[]] 188 | ## 2018-03-25 2.2.2 (iRon) 189 | - Updated 190 | - PowerShell Gallery Release 191 | ## 2018-03-25 2.2.1 (iRon) 192 | - New feature 193 | - Support for adding new properties (see: `-Property`) 194 | ## 2018-03-15 2.2.0 (iRon) 195 | - Updated 196 | - Read single records from the pipeline 197 | ## 2018-03-01 2.1.0 (iRon) 198 | - Fixes 199 | - Resolved: "Unexpected results when reusing custom objects in the pipeline" 200 | ## 2017-12-11 2.0.2 (iRon) 201 | - Updated 202 | - Reworked for PowerSnippets.com 203 | ## 2017-10-24 1.1.1 (iRon) 204 | - Fixes 205 | - Resolved bug where the Left Table contains a single column 206 | ## 2017-08-08 1.1.0 (iRon) 207 | - Updated 208 | - Merged the `-Expressions` and `-DefaultExpression` parameters 209 | ## 2017-01-01 0.99.99 (iRon) 210 | - Updated 211 | - First releases 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Join-Object 3 | 4 | Combines two object lists based on a related property between them. 5 | 6 | ## Syntax 7 | 8 | ```PowerShell 9 | Join-Object 10 | [-LeftObject ] 11 | [-RightObject ] 12 | [-On = @()] 13 | [-Equals = @()] 14 | [-Discern ] 15 | [-Property ] 16 | [-Where ] 17 | [-JoinType = 'Inner'] 18 | [-ValueName = ''] 19 | [-Strict] 20 | [-MatchCase] 21 | [] 22 | ``` 23 | 24 | ```PowerShell 25 | Join-Object 26 | [-LeftObject ] 27 | [-RightObject ] 28 | [-Using ] 29 | [-Discern ] 30 | [-Property ] 31 | [-Where ] 32 | [-JoinType = 'Inner'] 33 | [-ValueName = ''] 34 | [] 35 | ``` 36 | 37 | ## Description 38 | 39 | [This module][1] provides a set of commands to combine the properties from one or more objects. 40 | It creates a set that can be saved as a new object or used as it is. 41 | An object join is a means for combining properties from one (self-join) or more object lists 42 | by using values common to each. 43 | 44 | Main features: 45 | * An intuitive idiomatic PowerShell syntax 46 | * SQL like joining features 47 | * Smart property merging 48 | * Predefined join commands for updating, merging and specific join types 49 | * Well defined pipeline for the (left) input objects and output objects (streaming preserves memory) 50 | * Performs about twice as fast as Compare-Object on large object lists 51 | * Supports a list of (custom) objects, strings or primitives and dictionaries (e.g. hash tables) and data tables for input 52 | * Smart properties and calculated property expressions 53 | * Custom relation expressions 54 | * Supports PowerShell for Windows (5.1) and PowerShell Core 55 | 56 | The Join-Object cmdlet reveals the following proxy commands and aliases with their own 57 | ([`-JoinType`](#-jointype) and [`-Property`](#-property)) defaults: 58 | * `InnerJoin` (Alias `InnerJoin-Object` or `Join`), combines the related objects 59 | * `LeftJoin` (Alias `LeftJoin-Object`), combines the related objects and adds the rest of the left objects 60 | * `RightJoin` (Alias `RightJoin-Object`), combines the related objects and adds the rest of the right objects 61 | * `OuterJoin` (Alias `OuterJoin-Object`), returns the symmetric difference of the unrelated objects 62 | * `FullJoin` (Alias `FullJoin-Object`), combines the related objects and adds the rest of the left and right objects 63 | * `CrossJoin` (Alias `CrossJoin-Object`), combines each left object with each right object 64 | * `Update-Object` (Alias `Update`), updates the left object with the related right object 65 | * `Merge-Object` (Alias `Merge`), updates the left object with the related right object and adds the rest of the new (unrelated) right objects 66 | * `Get-Difference` (Alias `Differs`), returns the symmetric different objects and their properties 67 | 68 | > [!Note] 69 | > Some unapproved verb cmdlets (as e.g. `FullJoin-Object`) are provided as aliases to prevent a 70 | > "*unapproved verbs*" warning during the module import. For details see: [PowerShell\issues\25642][2]. 71 | 72 | ## Examples 73 | 74 | ### Example 1: Common (inner) join 75 | 76 | The following example will show an inner join based on the `country` property. 77 | Given the following object lists: 78 | 79 | ```PowerShell 80 | $Employee 81 | 82 | Id Name Country Department Age ReportsTo 83 | -- ---- ------- ---------- --- --------- 84 | 1 Aerts Belgium Sales 40 5 85 | 2 Bauer Germany Engineering 31 4 86 | 3 Cook England Sales 69 1 87 | 4 Duval France Engineering 21 5 88 | 5 Evans England Marketing 35 89 | 6 Fischer Germany Engineering 29 4 90 | 91 | $Department 92 | 93 | Name Country 94 | ---- ------- 95 | Engineering Germany 96 | Marketing England 97 | Sales France 98 | Purchase France 99 | 100 | 101 | $Employee | Join $Department -On Country | Format-Table 102 | 103 | Id Name Country Department Age ReportsTo 104 | -- ---- ------- ---------- --- --------- 105 | 2 {Bauer, Engineering} Germany Engineering 31 4 106 | 3 {Cook, Marketing} England Sales 69 1 107 | 4 {Duval, Sales} France Engineering 21 5 108 | 4 {Duval, Purchase} France Engineering 21 5 109 | 5 {Evans, Marketing} England Marketing 35 110 | 6 {Fischer, Engineering} Germany Engineering 29 4 111 | ``` 112 | 113 | ### Example 2: Full join overlapping column names 114 | 115 | 116 | The example below does a full join of the tables mentioned in the first example based 117 | on the `department` name and splits the duplicate (`country`) names over different properties. 118 | 119 | ```PowerShell 120 | $Employee | InnerJoin $Department -On Department -Equals Name -Discern Employee, Department | Format-Table 121 | 122 | Id Name EmployeeCountry DepartmentCountry Department Age ReportsTo 123 | -- ---- --------------- ----------------- ---------- --- --------- 124 | 1 Aerts Belgium France Sales 40 5 125 | 2 Bauer Germany Germany Engineering 31 4 126 | 3 Cook England France Sales 69 1 127 | 4 Duval France Germany Engineering 21 5 128 | 5 Evans England England Marketing 35 129 | 6 Fischer Germany Germany Engineering 29 4 130 | ``` 131 | 132 | ### Example 3: merge a table with updates 133 | 134 | 135 | This example merges the following `$Changes` list into the `$Employee` list of the first example. 136 | 137 | ```PowerShell 138 | $Changes 139 | 140 | Id Name Country Department Age ReportsTo 141 | -- ---- ------- ---------- --- --------- 142 | 3 Cook England Sales 69 5 143 | 6 Fischer France Engineering 29 4 144 | 7 Geralds Belgium Sales 71 1 145 | 146 | # Apply the changes to the employees 147 | $Employee | Merge $Changes -On Id | Format-Table 148 | 149 | Id Name Country Department Age ReportsTo 150 | -- ---- ------- ---------- --- --------- 151 | 1 Aerts Belgium Sales 40 5 152 | 2 Bauer Germany Engineering 31 4 153 | 3 Cook England Sales 69 5 154 | 4 Duval France Engineering 21 5 155 | 5 Evans England Marketing 35 156 | 6 Fischer France Engineering 29 4 157 | 7 Geralds Belgium Sales 71 1 158 | ``` 159 | 160 | ### Example 4: Self join 161 | 162 | 163 | This example shows a (self)join where each employee is connected with another employee on the country. 164 | 165 | ```PowerShell 166 | $Employee | Join -On Country -Discern *1,*2 | Format-Table * 167 | 168 | Id1 Id2 Name1 Name2 Country Department1 Department2 Age1 Age2 ReportsTo1 ReportsTo2 169 | --- --- ----- ----- ------- ----------- ----------- ---- ---- ---------- ---------- 170 | 2 6 Bauer Fischer Germany Engineering Engineering 31 29 4 4 171 | 3 5 Cook Evans England Sales Marketing 69 35 1 172 | 5 3 Evans Cook England Marketing Sales 35 69 1 173 | 6 2 Fischer Bauer Germany Engineering Engineering 29 31 4 4 174 | ``` 175 | 176 | ### Example 5: Join a scalar array 177 | 178 | 179 | This example adds an Id to the department list. 180 | 181 | > [!Note] 182 | > The default column name of (nameless) scalar array is `` this will show when the [`-ValueName`](#-valuename) parameter is omitted. 183 | 184 | ```PowerShell 185 | 1..9 | Join $Department -ValueName Id 186 | 187 | Id Name Country 188 | -- ---- ------- 189 | 1 Engineering Germany 190 | 2 Marketing England 191 | 3 Sales France 192 | 4 Purchase France 193 | ``` 194 | 195 | ### Example 6: Transpose arrays 196 | 197 | 198 | The following example, the `join-Object` cmdlet (`... | Join`) joins multiple arrays to a collection array. 199 | The [`Foreach-Object`](https://go.microsoft.com/fwlink/?LinkID=2096867) cmdlet iterates over the rows and the `-Join` operator concatenates the item collections 200 | 201 | ```PowerShell 202 | $a = 'a1', 'a2', 'a3', 'a4' 203 | $b = 'b1', 'b2', 'b3', 'b4' 204 | $c = 'c1', 'c2', 'c3', 'c4' 205 | $d = 'd1', 'd2', 'd3', 'd4' 206 | 207 | $a | Join $b | Join $c | Join $d | % { $_ -Join '|' } 208 | 209 | a1|b1|c1|d1 210 | a2|b2|c2|d2 211 | a3|b3|c3|d3 212 | a4|b4|c4|d4 213 | ``` 214 | 215 | ### Example 7: Arrays to objects 216 | 217 | 218 | This example will change the collections of the previous example into objects with named properties. 219 | 220 | ```PowerShell 221 | $a | Join $b | Join $c | Join $d -Name a, b, c, d 222 | 223 | a b c d 224 | - - - - 225 | a1 b1 c1 d1 226 | a2 b2 c2 d2 227 | a3 b3 c3 d3 228 | a4 b4 c4 d4 229 | ``` 230 | 231 | ## Parameters 232 | 233 | ### `-LeftObject` <Object> 234 | 235 | The left object list, usually provided through the pipeline, to be joined. 236 | 237 | > [!Note] 238 | > A **self-join** on the `LeftObject` list will be performed if the `RightObject` is omitted. 239 | 240 | ```powershell 241 | Name: -LeftObject 242 | Aliases: # None 243 | Type: [Object] 244 | Value (default): # Undefined 245 | Parameter sets: On, Using 246 | Mandatory: False 247 | Position: # Named 248 | Accept pipeline input: False 249 | Accept wildcard characters: False 250 | ``` 251 | 252 | ### `-RightObject` <Object> 253 | 254 | The right object list, provided by the first argument, to be joined. 255 | 256 | > [!Note] 257 | > A **self-join** on the `RightObject` list will be performed if the `LeftObject` is omitted. 258 | 259 | ```powershell 260 | Name: -RightObject 261 | Aliases: # None 262 | Type: [Object] 263 | Value (default): # Undefined 264 | Parameter sets: On, Using 265 | Mandatory: False 266 | Position: # Named 267 | Accept pipeline input: False 268 | Accept wildcard characters: False 269 | ``` 270 | 271 | ### `-On` <Array> 272 | 273 | The [`-On`](#-on) parameter defines which objects should be joined together. 274 | If the [`-Equals`](#-equals) parameter is omitted, the value(s) of the properties listed by the [`-On`](#-on) parameter should be 275 | equal at both sides in order to join the left object with the right object. 276 | If the [`-On`](#-on) parameter contains an expression, the expression will be evaluated where `$_`, `$PSItem` and 277 | `$Left` contains the current object. The result of the expression will be compared to right object property 278 | defined by the [`-Equals`](#-equals) parameter. 279 | 280 | > [!Note] 281 | > The list of properties defined by the [`-On`](#-on) parameter will be complemented with the list of 282 | > properties defined by the [`-Equals`](#-equals) parameter and vice versa. 283 | 284 | > [!Note] 285 | > Related properties will be merged to a single property by default (see also the [`-Property`](#-property) parameter). 286 | 287 | > [!Tip] 288 | > If the [`-On`](#-on) and the [`-Using`](#-using) parameter are omitted, a side-by-side join is returned unless 289 | > `OuterJoin` is performed where the default [`-On`](#-on) parameter value is * (all properties). 290 | 291 | > [!Tip] 292 | > if the left object is a scalar array, the [`-On`](#-on) parameters is used to name the scalar array. 293 | 294 | ```powershell 295 | Name: -On 296 | Aliases: # None 297 | Type: [Array] 298 | Value (default): @() 299 | Parameter sets: On 300 | Mandatory: False 301 | Position: # Named 302 | Accept pipeline input: False 303 | Accept wildcard characters: False 304 | ``` 305 | 306 | ### `-Using` <ScriptBlock> 307 | 308 | Any conditional expression that requires to evaluate to true in order to join the left object with the 309 | right object. 310 | 311 | The following variables are exposed for a (ScriptBlock) expression: 312 | * `$_`: iterates each property name 313 | * `$Left`: a hash table representing the current left object (each self-contained [`-LeftObject`](#-leftobject)). 314 | The hash table will be empty (`@{}`) in the outer part of a left join or full join. 315 | * `$LeftIndex`: the index of the left object (`$Null` in the outer part of a right- or full join) 316 | * `$Right`: a hash table representing the current right object (each self-contained [`-RightObject`](#-rightobject)) 317 | The hash table will be empty (`@{}`) in the outer part of a right join or full join. 318 | * `$RightIndex`: the index of the right object (`$Null` in the outer part of a left- or full join) 319 | 320 | > [!Warning] 321 | > The [`-Using`](#-using) parameter has the most complex comparison possibilities but is considerable slower 322 | > than the [`-On`](#-on) parameter. 323 | 324 | > [!Note] 325 | > The [`-Using`](#-using) parameter cannot be used with the [`-On`](#-on) parameter. 326 | 327 | ```powershell 328 | Name: -Using 329 | Aliases: # None 330 | Type: [ScriptBlock] 331 | Value (default): # Undefined 332 | Parameter sets: Using 333 | Mandatory: False 334 | Position: # Named 335 | Accept pipeline input: False 336 | Accept wildcard characters: False 337 | ``` 338 | 339 | ### `-Equals` <Array> 340 | 341 | If the [`-Equals`](#-equals) parameter is supplied, the value(s) of the left object properties listed by the [`-On`](#-on) 342 | parameter should be equal to the value(s) of the right object listed by the [`-Equals`](#-equals) parameter in order to 343 | join the left object with the right object. 344 | If the [`-Equals`](#-equals) parameter contains an expression, the expression will be evaluated where `$_`, `$PSItem` and 345 | `$Right` contains the current object. The result of the expression will be compared to left object property 346 | defined by the [`-On`](#-on) parameter. 347 | 348 | > [!Note] 349 | > The list of properties defined by the [-Equal] parameter will be complemented with the list of properties 350 | > defined by the -On parameter and vice versa. This means that by default value of the [`-Equals`](#-equals) parameter 351 | > is equal to the value supplied to the [`-On`](#-on) parameter. 352 | 353 | > [!Note] 354 | > A property will be omitted in the results if it exists on both sides and if the property at the other side 355 | > is related to another property. 356 | 357 | > [!Note] 358 | > The [`-Equals`](#-equals) parameter can only be used with the [`-On`](#-on) parameter. 359 | 360 | > [!Tip] 361 | > if the right object is a scalar array, the [`-Equals`](#-equals) parameters is used to name the scalar array. 362 | 363 | ```powershell 364 | Name: -Equals 365 | Aliases: -Eq 366 | Type: [Array] 367 | Value (default): @() 368 | Parameter sets: On 369 | Mandatory: False 370 | Position: # Named 371 | Accept pipeline input: False 372 | Accept wildcard characters: False 373 | ``` 374 | 375 | ### `-Discern` <String[]> 376 | 377 | By default unrelated properties with the same name will be collected in a single object property. 378 | The [`-Discern`](#-discern) parameter (alias [-NameItems]) defines how to rename the object properties and divide 379 | them over multiple properties. If a given name pattern contains an asterisks (`*`), the asterisks 380 | will be replaced with the original property name. Otherwise, the property name for each property 381 | item will be prefixed with the given name pattern. 382 | 383 | The property collection of multiple (chained) join commands can be divided in once from the last join 384 | command in the change. The rename patterns are right aligned, meaning that the last renamed pattern 385 | will be applied to the last object joined. If there are less rename patterns than property items, 386 | the rest of the (left most) property items will be put in a fixed array under the original property name. 387 | 388 | > [!Note] 389 | > As apposed to the [`-On`](#-on) parameter, properties with the same name on both sides will not be renamed. 390 | 391 | > [!Note] 392 | > Related properties (with an equal value defined by the [`-On`](#-on) parameter) will be merged to a single item. 393 | 394 | ```powershell 395 | Name: -Discern 396 | Aliases: -NameItems 397 | Type: [String[]] 398 | Value (default): # Undefined 399 | Parameter sets: On, Using 400 | Mandatory: False 401 | Position: # Named 402 | Accept pipeline input: False 403 | Accept wildcard characters: False 404 | ``` 405 | 406 | ### `-Property` <Object> 407 | 408 | A hash table or list of property names (strings) and/or hash tables that define a new selection of 409 | property names and values 410 | 411 | Hash tables should be in the format `@{ = }` where the `` is a 412 | ScriptBlock or a smart property (string) and defines how the specific left and right properties should be 413 | merged. See the [`-Using`](#-using) parameter for available expression variables. 414 | 415 | The following smart properties are available: 416 | * A general property: '', where `` represents the property name of the left 417 | and/or right property, e.g. `@{ MyProperty = 'Name' }`. If the property exists on both sides, an array 418 | holding both values will be returned. In the outer join, the value of the property will be `$Null`. 419 | This smart property is similar to the expression: `@{ MyProperty = { @($Left['Name'], $Right['Name']) } }` 420 | * A general wildcard property: `'*'`, where `* `represents the property name of the current property, e.g. 421 | `MyProperty` in `@{ MyProperty = '*' }`. If the property exists on both sides: 422 | - and the properties are unrelated, an array holding both values will be returned 423 | - and the properties are related to each other, the (equal) values will be merged in one property value 424 | - and the property at the other side is related to an different property, the property is omitted 425 | The argument: `-Property *`, will apply a general wildcard on all left and right properties. 426 | * A left property: `;Left.'`, or right property: `;Right.'`, where 427 | `` represents the property name of the either the left or right property. If the property 428 | doesn't exist, the value of the property will be `$Null`. 429 | * A left wildcard property: `'Left.*'`, or right wildcard property: `Right.*`, where `*` represents the 430 | property name of the current the left or right property, e.g. `MyProperty` in `@{ MyProperty = 'Left.*' }`. 431 | If the property doesn't exist (in an outer join), the property with the same name at the other side will 432 | be taken. If the property doesn't exist on either side, the value of the property will be `$Null`. 433 | The argument: `-Property 'Left.*'`, will apply a left wildcard property on all the left object properties. 434 | 435 | If the [`-Property`](#-property) parameter and the [`-Discern`](#-discern) parameter are omitted, a general wildcard property is applied 436 | on all the left and right properties. 437 | 438 | The last defined expression or smart property will overrule any previous defined properties. 439 | 440 | ```powershell 441 | Name: -Property 442 | Aliases: # None 443 | Type: [Object] 444 | Value (default): # Undefined 445 | Parameter sets: On, Using 446 | Mandatory: False 447 | Position: # Named 448 | Accept pipeline input: False 449 | Accept wildcard characters: False 450 | ``` 451 | 452 | ### `-Where` <ScriptBlock> 453 | 454 | An expression that defines the condition to be met for the objects to be returned. See the [`-Using`](#-using) 455 | parameter for available expression variables. 456 | 457 | ```powershell 458 | Name: -Where 459 | Aliases: # None 460 | Type: [ScriptBlock] 461 | Value (default): # Undefined 462 | Parameter sets: On, Using 463 | Mandatory: False 464 | Position: # Named 465 | Accept pipeline input: False 466 | Accept wildcard characters: False 467 | ``` 468 | 469 | ### `-JoinType` <String> 470 | 471 | Defines which unrelated objects should be included (see: [Description](#description)). 472 | Valid values are: `Inner`, `Left`, `Right`, `Full` or `Cross`. The default is `Inner`. 473 | 474 | > [!Tip] 475 | > it is recommended to use the related proxy commands (`... | -Object ...`) instead. 476 | 477 | ```powershell 478 | Name: -JoinType 479 | Aliases: # None 480 | Type: [String] 481 | Value (default): 'Inner' 482 | Parameter sets: On, Using 483 | Mandatory: False 484 | Position: # Named 485 | Accept pipeline input: False 486 | Accept wildcard characters: False 487 | ``` 488 | 489 | ### `-ValueName` <String> 490 | 491 | Defines the name of the added property in case a scalar array is joined with an object array. 492 | The default property name for each scalar is: ``. 493 | 494 | > [!Note] 495 | > if two scalar (or collection) arrays are joined, an array of (PSObject) collections is returned. 496 | Each collection is a concatenation of the left item (collection) and the right item (collection). 497 | 498 | ```powershell 499 | Name: -ValueName 500 | Aliases: # None 501 | Type: [String] 502 | Value (default): '' 503 | Parameter sets: On, Using 504 | Mandatory: False 505 | Position: # Named 506 | Accept pipeline input: False 507 | Accept wildcard characters: False 508 | ``` 509 | 510 | ### `-Strict` 511 | 512 | If the [`-Strict`](#-strict) switch is set, the comparison between the related properties defined by the [`-On`](#-on) Parameter 513 | (and the [`-Equals`](#-equals) parameter) is based on a strict equality (both type and value need to be equal). 514 | 515 | ```powershell 516 | Name: -Strict 517 | Aliases: # None 518 | Type: [SwitchParameter] 519 | Value (default): # Undefined 520 | Parameter sets: On 521 | Mandatory: False 522 | Position: # Named 523 | Accept pipeline input: False 524 | Accept wildcard characters: False 525 | ``` 526 | 527 | ### `-MatchCase` 528 | 529 | If the [`-MatchCase`](#-matchcase) (alias `-CaseSensitive`) switch is set, the comparison between the related properties 530 | defined by the [`-On`](#-on) Parameter (and the [`-Equals`](#-equals) parameter) will case sensitive. 531 | 532 | ```powershell 533 | Name: -MatchCase 534 | Aliases: -CaseSensitive 535 | Type: [SwitchParameter] 536 | Value (default): # Undefined 537 | Parameter sets: On 538 | Mandatory: False 539 | Position: # Named 540 | Accept pipeline input: False 541 | Accept wildcard characters: False 542 | ``` 543 | 544 | ## Related Links 545 | 546 | * [Join-Object on GitHub](https://github.com/iRon7/Join-Object) 547 | * [Give the script author the ability to disable the unapproved verbs warning](https://github.com/PowerShell/PowerShell/issues/25642) 548 | 549 | 550 | * [Please give a thumbs up if you like to support the proposal to 'Add a Join-Object cmdlet to the standard PowerShell equipment'](https://github.com/PowerShell/PowerShell/issues/14994) 551 | 552 | 553 | 554 | [1]: https://github.com/iRon7/Join-Object "Join-Object on GitHub" 555 | [2]: https://github.com/PowerShell/PowerShell/issues/25642 "Give the script author the ability to disable the unapproved verbs warning" 556 | 557 | [comment]: <> (Created with Get-MarkdownHelp: Install-Script -Name Get-MarkdownHelp) 558 | -------------------------------------------------------------------------------- /Join.psm1: -------------------------------------------------------------------------------- 1 | <#PSScriptInfo 2 | .VERSION 3.8.4 3 | .GUID 54688e75-298c-4d4b-a2d0-d478e6069126 4 | .AUTHOR Ronald Bode (iRon) 5 | .DESCRIPTION Join-Object combines two object lists based on a related property between them. 6 | .COMPANYNAME PowerSnippets.com 7 | .COPYRIGHT Ronald Bode (iRon) 8 | .TAGS Join-Object Join InnerJoin LeftJoin RightJoin FullJoin OuterJoin CrossJoin Update Merge Difference Combine Table 9 | .LICENSEURI https://github.com/iRon7/Join-Object/LICENSE 10 | .PROJECTURI https://github.com/iRon7/Join-Object 11 | .ICONURI https://raw.githubusercontent.com/iRon7/Join-Object/master/Join-Object.png 12 | .EXTERNALMODULEDEPENDENCIES 13 | .REQUIREDSCRIPTS 14 | .EXTERNALSCRIPTDEPENDENCIES 15 | .RELEASENOTES To install the new Join module equivalent: Install-Module -Name JoinModule 16 | .PRIVATEDATA 17 | #> 18 | 19 | using namespace System.Management.Automation 20 | using namespace System.Collections 21 | using namespace System.Collections.Generic 22 | using namespace System.Collections.ObjectModel 23 | 24 | <# 25 | .SYNOPSIS 26 | Combines two object lists based on a related property between them. 27 | 28 | .DESCRIPTION 29 | [This module][1] provides a set of commands to combine the properties from one or more objects. 30 | It creates a set that can be saved as a new object or used as it is. 31 | An object join is a means for combining properties from one (self-join) or more object lists 32 | by using values common to each. 33 | 34 | Main features: 35 | * An intuitive idiomatic PowerShell syntax 36 | * SQL like joining features 37 | * Smart property merging 38 | * Predefined join commands for updating, merging and specific join types 39 | * Well defined pipeline for the (left) input objects and output objects (streaming preserves memory) 40 | * Performs about twice as fast as Compare-Object on large object lists 41 | * Supports a list of (custom) objects, strings or primitives and dictionaries (e.g. hash tables) and data tables for input 42 | * Smart properties and calculated property expressions 43 | * Custom relation expressions 44 | * Supports PowerShell for Windows (5.1) and PowerShell Core 45 | 46 | The Join-Object cmdlet reveals the following proxy commands and aliases with their own 47 | ([-JoinType] and [-Property]) defaults: 48 | * `InnerJoin` (Alias `InnerJoin-Object` or `Join`), combines the related objects 49 | * `LeftJoin` (Alias `LeftJoin-Object`), combines the related objects and adds the rest of the left objects 50 | * `RightJoin` (Alias `RightJoin-Object`), combines the related objects and adds the rest of the right objects 51 | * `OuterJoin` (Alias `OuterJoin-Object`), returns the symmetric difference of the unrelated objects 52 | * `FullJoin` (Alias `FullJoin-Object`), combines the related objects and adds the rest of the left and right objects 53 | * `CrossJoin` (Alias `CrossJoin-Object`), combines each left object with each right object 54 | * `Update-Object` (Alias `Update`), updates the left object with the related right object 55 | * `Merge-Object` (Alias `Merge`), updates the left object with the related right object and adds the rest of the new (unrelated) right objects 56 | * `Get-Difference` (Alias `Differs`), returns the symmetric different objects and their properties 57 | 58 | > [!Note] 59 | > Some unapproved verb cmdlets (as e.g. `FullJoin-Object`) are provided as aliases to prevent a 60 | > "*unapproved verbs*" warning during the module import. For details see: [PowerShell\issues\25642][2]. 61 | 62 | .PARAMETER LeftObject 63 | The left object list, usually provided through the pipeline, to be joined. 64 | 65 | > [!Note] 66 | > A **self-join** on the `LeftObject` list will be performed if the `RightObject` is omitted. 67 | 68 | .PARAMETER RightObject 69 | The right object list, provided by the first argument, to be joined. 70 | 71 | > [!Note] 72 | > A **self-join** on the `RightObject` list will be performed if the `LeftObject` is omitted. 73 | 74 | .PARAMETER On 75 | The [-On] parameter defines which objects should be joined together.\ 76 | If the [-Equals] parameter is omitted, the value(s) of the properties listed by the [-On] parameter should be 77 | equal at both sides in order to join the left object with the right object.\ 78 | If the [-On] parameter contains an expression, the expression will be evaluated where `$_`, `$PSItem` and 79 | `$Left` contains the current object. The result of the expression will be compared to right object property 80 | defined by the [-Equals] parameter. 81 | 82 | > [!Note] 83 | > The list of properties defined by the [-On] parameter will be complemented with the list of 84 | > properties defined by the [-Equals] parameter and vice versa. 85 | 86 | > [!Note] 87 | > Related properties will be merged to a single property by default (see also the [-Property] parameter). 88 | 89 | > [!Tip] 90 | > If the [-On] and the [-Using] parameter are omitted, a side-by-side join is returned unless 91 | > `OuterJoin` is performed where the default [-On] parameter value is * (all properties). 92 | 93 | > [!Tip] 94 | > if the left object is a scalar array, the [-On] parameters is used to name the scalar array. 95 | 96 | .PARAMETER Equals 97 | If the [-Equals] parameter is supplied, the value(s) of the left object properties listed by the [-On] 98 | parameter should be equal to the value(s) of the right object listed by the [-Equals] parameter in order to 99 | join the left object with the right object.\ 100 | If the [-Equals] parameter contains an expression, the expression will be evaluated where `$_`, `$PSItem` and 101 | `$Right` contains the current object. The result of the expression will be compared to left object property 102 | defined by the [-On] parameter. 103 | 104 | > [!Note] 105 | > The list of properties defined by the [-Equal] parameter will be complemented with the list of properties 106 | > defined by the -On parameter and vice versa. This means that by default value of the [-Equals] parameter 107 | > is equal to the value supplied to the [-On] parameter. 108 | 109 | > [!Note] 110 | > A property will be omitted in the results if it exists on both sides and if the property at the other side 111 | > is related to another property. 112 | 113 | > [!Note] 114 | > The [-Equals] parameter can only be used with the [-On] parameter. 115 | 116 | > [!Tip] 117 | > if the right object is a scalar array, the [-Equals] parameters is used to name the scalar array. 118 | 119 | .PARAMETER Strict 120 | If the [-Strict] switch is set, the comparison between the related properties defined by the [-On] Parameter 121 | (and the [-Equals] parameter) is based on a strict equality (both type and value need to be equal). 122 | 123 | .PARAMETER MatchCase 124 | If the [-MatchCase] (alias `-CaseSensitive`) switch is set, the comparison between the related properties 125 | defined by the [-On] Parameter (and the [-Equals] parameter) will case sensitive. 126 | 127 | .PARAMETER Using 128 | Any conditional expression that requires to evaluate to true in order to join the left object with the 129 | right object. 130 | 131 | The following variables are exposed for a (ScriptBlock) expression: 132 | * `$_`: iterates each property name 133 | * `$Left`: a hash table representing the current left object (each self-contained [-LeftObject]). 134 | The hash table will be empty (`@{}`) in the outer part of a left join or full join. 135 | * `$LeftIndex`: the index of the left object (`$Null` in the outer part of a right- or full join) 136 | * `$Right`: a hash table representing the current right object (each self-contained [-RightObject]) 137 | The hash table will be empty (`@{}`) in the outer part of a right join or full join. 138 | * `$RightIndex`: the index of the right object (`$Null` in the outer part of a left- or full join) 139 | 140 | 141 | > [!Warning] 142 | > The [-Using] parameter has the most complex comparison possibilities but is considerable slower 143 | > than the [-On] parameter. 144 | 145 | > [!Note] 146 | > The [-Using] parameter cannot be used with the [-On] parameter. 147 | 148 | .PARAMETER Where 149 | An expression that defines the condition to be met for the objects to be returned. See the [-Using] 150 | parameter for available expression variables. 151 | 152 | .PARAMETER Discern 153 | By default unrelated properties with the same name will be collected in a single object property. 154 | The [-Discern] parameter (alias [-NameItems]) defines how to rename the object properties and divide 155 | them over multiple properties. If a given name pattern contains an asterisks (`*`), the asterisks 156 | will be replaced with the original property name. Otherwise, the property name for each property 157 | item will be prefixed with the given name pattern. 158 | 159 | The property collection of multiple (chained) join commands can be divided in once from the last join 160 | command in the change. The rename patterns are right aligned, meaning that the last renamed pattern 161 | will be applied to the last object joined. If there are less rename patterns than property items, 162 | the rest of the (left most) property items will be put in a fixed array under the original property name. 163 | 164 | > [!Note] 165 | > As apposed to the [-On] parameter, properties with the same name on both sides will not be renamed. 166 | 167 | > [!Note] 168 | > Related properties (with an equal value defined by the [-On] parameter) will be merged to a single item. 169 | 170 | .PARAMETER Property 171 | A hash table or list of property names (strings) and/or hash tables that define a new selection of 172 | property names and values 173 | 174 | Hash tables should be in the format `@{ = }` where the `` is a 175 | ScriptBlock or a smart property (string) and defines how the specific left and right properties should be 176 | merged. See the [-Using] parameter for available expression variables. 177 | 178 | The following smart properties are available: 179 | * A general property: '', where `` represents the property name of the left 180 | and/or right property, e.g. `@{ MyProperty = 'Name' }`. If the property exists on both sides, an array 181 | holding both values will be returned. In the outer join, the value of the property will be `$Null`. 182 | This smart property is similar to the expression: `@{ MyProperty = { @($Left['Name'], $Right['Name']) } }` 183 | * A general wildcard property: `'*'`, where `* `represents the property name of the current property, e.g. 184 | `MyProperty` in `@{ MyProperty = '*' }`. If the property exists on both sides: 185 | - and the properties are unrelated, an array holding both values will be returned 186 | - and the properties are related to each other, the (equal) values will be merged in one property value 187 | - and the property at the other side is related to an different property, the property is omitted 188 | The argument: `-Property *`, will apply a general wildcard on all left and right properties. 189 | * A left property: `;Left.'`, or right property: `;Right.'`, where 190 | `` represents the property name of the either the left or right property. If the property 191 | doesn't exist, the value of the property will be `$Null`. 192 | * A left wildcard property: `'Left.*'`, or right wildcard property: `Right.*`, where `*` represents the 193 | property name of the current the left or right property, e.g. `MyProperty` in `@{ MyProperty = 'Left.*' }`. 194 | If the property doesn't exist (in an outer join), the property with the same name at the other side will 195 | be taken. If the property doesn't exist on either side, the value of the property will be `$Null`. 196 | The argument: `-Property 'Left.*'`, will apply a left wildcard property on all the left object properties. 197 | 198 | If the [-Property] parameter and the [-Discern] parameter are omitted, a general wildcard property is applied 199 | on all the left and right properties. 200 | 201 | The last defined expression or smart property will overrule any previous defined properties. 202 | 203 | .PARAMETER ValueName 204 | Defines the name of the added property in case a scalar array is joined with an object array. 205 | The default property name for each scalar is: ``. 206 | 207 | > [!Note] 208 | > if two scalar (or collection) arrays are joined, an array of (PSObject) collections is returned. 209 | Each collection is a concatenation of the left item (collection) and the right item (collection). 210 | 211 | .PARAMETER JoinType 212 | Defines which unrelated objects should be included (see: [Description]). 213 | Valid values are: `Inner`, `Left`, `Right`, `Full` or `Cross`. The default is `Inner`. 214 | 215 | > [!Tip] 216 | > it is recommended to use the related proxy commands (`... | -Object ...`) instead. 217 | 218 | .EXAMPLE 219 | # Common (inner) join 220 | The following example will show an inner join based on the `country` property.\ 221 | Given the following object lists: 222 | 223 | PS C:\> $Employee 224 | 225 | Id Name Country Department Age ReportsTo 226 | -- ---- ------- ---------- --- --------- 227 | 1 Aerts Belgium Sales 40 5 228 | 2 Bauer Germany Engineering 31 4 229 | 3 Cook England Sales 69 1 230 | 4 Duval France Engineering 21 5 231 | 5 Evans England Marketing 35 232 | 6 Fischer Germany Engineering 29 4 233 | 234 | PS C:\> $Department 235 | 236 | Name Country 237 | ---- ------- 238 | Engineering Germany 239 | Marketing England 240 | Sales France 241 | Purchase France 242 | 243 | 244 | PS C:\> $Employee | Join $Department -On Country | Format-Table 245 | 246 | Id Name Country Department Age ReportsTo 247 | -- ---- ------- ---------- --- --------- 248 | 2 {Bauer, Engineering} Germany Engineering 31 4 249 | 3 {Cook, Marketing} England Sales 69 1 250 | 4 {Duval, Sales} France Engineering 21 5 251 | 4 {Duval, Purchase} France Engineering 21 5 252 | 5 {Evans, Marketing} England Marketing 35 253 | 6 {Fischer, Engineering} Germany Engineering 29 4 254 | 255 | .EXAMPLE 256 | # Full join overlapping column names 257 | 258 | The example below does a full join of the tables mentioned in the first example based 259 | on the `department` name and splits the duplicate (`country`) names over different properties. 260 | 261 | PS C:\> $Employee | InnerJoin $Department -On Department -Equals Name -Discern Employee, Department | Format-Table 262 | 263 | Id Name EmployeeCountry DepartmentCountry Department Age ReportsTo 264 | -- ---- --------------- ----------------- ---------- --- --------- 265 | 1 Aerts Belgium France Sales 40 5 266 | 2 Bauer Germany Germany Engineering 31 4 267 | 3 Cook England France Sales 69 1 268 | 4 Duval France Germany Engineering 21 5 269 | 5 Evans England England Marketing 35 270 | 6 Fischer Germany Germany Engineering 29 4 271 | 272 | .EXAMPLE 273 | # merge a table with updates 274 | 275 | This example merges the following `$Changes` list into the `$Employee` list of the first example. 276 | 277 | PS C:\> $Changes 278 | 279 | Id Name Country Department Age ReportsTo 280 | -- ---- ------- ---------- --- --------- 281 | 3 Cook England Sales 69 5 282 | 6 Fischer France Engineering 29 4 283 | 7 Geralds Belgium Sales 71 1 284 | 285 | PS C:\> # Apply the changes to the employees 286 | PS C:\> $Employee | Merge $Changes -On Id | Format-Table 287 | 288 | Id Name Country Department Age ReportsTo 289 | -- ---- ------- ---------- --- --------- 290 | 1 Aerts Belgium Sales 40 5 291 | 2 Bauer Germany Engineering 31 4 292 | 3 Cook England Sales 69 5 293 | 4 Duval France Engineering 21 5 294 | 5 Evans England Marketing 35 295 | 6 Fischer France Engineering 29 4 296 | 7 Geralds Belgium Sales 71 1 297 | 298 | .EXAMPLE 299 | # Self join 300 | 301 | This example shows a (self)join where each employee is connected with another employee on the country. 302 | 303 | PS C:\> $Employee | Join -On Country -Discern *1,*2 | Format-Table * 304 | 305 | Id1 Id2 Name1 Name2 Country Department1 Department2 Age1 Age2 ReportsTo1 ReportsTo2 306 | --- --- ----- ----- ------- ----------- ----------- ---- ---- ---------- ---------- 307 | 2 6 Bauer Fischer Germany Engineering Engineering 31 29 4 4 308 | 3 5 Cook Evans England Sales Marketing 69 35 1 309 | 5 3 Evans Cook England Marketing Sales 35 69 1 310 | 6 2 Fischer Bauer Germany Engineering Engineering 29 31 4 4 311 | 312 | .EXAMPLE 313 | # Join a scalar array 314 | 315 | This example adds an Id to the department list.\ 316 | 317 | > [!Note] 318 | > The default column name of (nameless) scalar array is `` this will show when the [-ValueName] parameter is omitted. 319 | 320 | PS C:\> 1..9 | Join $Department -ValueName Id 321 | 322 | Id Name Country 323 | -- ---- ------- 324 | 1 Engineering Germany 325 | 2 Marketing England 326 | 3 Sales France 327 | 4 Purchase France 328 | 329 | .EXAMPLE 330 | # Transpose arrays 331 | 332 | The following example, the `join-Object` cmdlet (`... | Join`) joins multiple arrays to a collection array.\ 333 | The [Foreach-Object] cmdlet iterates over the rows and the `-Join` operator concatenates the item collections 334 | 335 | PS C:\> $a = 'a1', 'a2', 'a3', 'a4' 336 | PS C:\> $b = 'b1', 'b2', 'b3', 'b4' 337 | PS C:\> $c = 'c1', 'c2', 'c3', 'c4' 338 | PS C:\> $d = 'd1', 'd2', 'd3', 'd4' 339 | 340 | PS C:\> $a | Join $b | Join $c | Join $d | % { $_ -Join '|' } 341 | 342 | a1|b1|c1|d1 343 | a2|b2|c2|d2 344 | a3|b3|c3|d3 345 | a4|b4|c4|d4 346 | 347 | .EXAMPLE 348 | # Arrays to objects 349 | 350 | This example will change the collections of the previous example into objects with named properties. 351 | 352 | PS C:\> $a | Join $b | Join $c | Join $d -Name a, b, c, d 353 | 354 | a b c d 355 | - - - - 356 | a1 b1 c1 d1 357 | a2 b2 c2 d2 358 | a3 b3 c3 d3 359 | a4 b4 c4 d4 360 | 361 | .LINK 362 | [1]: https://github.com/iRon7/Join-Object "Join-Object on GitHub" 363 | [2]: https://github.com/PowerShell/PowerShell/issues/25642 "Give the script author the ability to disable the unapproved verbs warning" 364 | 365 | https://github.com/PowerShell/PowerShell/issues/14994 "Please give a thumbs up if you like to support the proposal to 'Add a Join-Object cmdlet to the standard PowerShell equipment'" 366 | #> 367 | 368 | function Join-Object { 369 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('InjectionRisk.Create', '', Scope = 'Function')] 370 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('InjectionRisk.ForeachObjectInjection', '', Scope = 'Function')] 371 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseLiteralInitializerForHashtable', '', Scope = 'Function')] 372 | [CmdletBinding(DefaultParameterSetName = 'On')][OutputType([Object[]])] param( 373 | 374 | [Parameter(ValueFromPipeLine = $True, ParameterSetName = 'On')] 375 | [Parameter(ValueFromPipeLine = $True, ParameterSetName = 'Using')] 376 | $LeftObject, 377 | 378 | [Parameter(Position = 0, ParameterSetName = 'On')] 379 | [Parameter(Position = 0, ParameterSetName = 'Using')] 380 | $RightObject, 381 | 382 | [Parameter(Position = 1, ParameterSetName = 'On')] 383 | [array]$On = @(), 384 | 385 | [Parameter(Position = 1, ParameterSetName = 'Using')] 386 | [scriptblock]$Using, 387 | 388 | [Parameter(ParameterSetName = 'On')] 389 | [Alias('Eq')][array]$Equals = @(), 390 | 391 | [Parameter(Position = 2, ParameterSetName = 'On')] 392 | [Parameter(Position = 2, ParameterSetName = 'Using')] 393 | [Alias('NameItems')][AllowEmptyString()][String[]]$Discern, 394 | 395 | [Parameter(ParameterSetName = 'On')] 396 | [Parameter(ParameterSetName = 'Using')] 397 | $Property, 398 | 399 | [Parameter(Position = 3, ParameterSetName = 'On')] 400 | [Parameter(Position = 3, ParameterSetName = 'Using')] 401 | [scriptblock]$Where, 402 | 403 | [Parameter(ParameterSetName = 'On')] 404 | [Parameter(ParameterSetName = 'Using')] 405 | [ValidateSet('Inner', 'Left', 'Right', 'Full', 'Outer', 'Cross')][String]$JoinType = 'Inner', 406 | 407 | [Parameter(ParameterSetName = 'On')] 408 | [Parameter(ParameterSetName = 'Using')] 409 | [string]$ValueName = '', 410 | 411 | [Parameter(ParameterSetName = 'On')] 412 | [switch]$Strict, 413 | 414 | [Parameter(ParameterSetName = 'On')] 415 | [Alias('CaseSensitive')][switch]$MatchCase 416 | ) 417 | begin { 418 | trap { $PSCmdlet.ThrowTerminatingError($_) } 419 | $Esc = "`u{1B}``" 420 | 421 | function AsDictionary { 422 | param( 423 | [Parameter(ValueFromPipeLine = $True)]$Object, 424 | $ValueName 425 | ) 426 | begin { 427 | $Keys = $Null 428 | } 429 | process { 430 | if ($Null -eq $Keys) { 431 | $Keys = 432 | if ($Null -eq $Object) { ,@() } 433 | elseif ($Object.GetType().GetElementType() -and $Object.get_Count() -eq 0) { $Null } 434 | else { 435 | $1 = $Object | Select-Object -First 1 436 | if ($1 -is [string] -or $1 -is [ValueType] -or $1 -is [Array]) { $Null } 437 | elseif ($1 -is [Collection[PSObject]]) { $Null } 438 | elseif ($1 -is [Data.DataRow]) { ,@($1.Table.Columns.ColumnName) } 439 | elseif ($1 -is [IDictionary]) { ,@($1.Get_Keys()) } 440 | elseif ($1) { ,@($1.PSObject.Properties.Name) } 441 | } 442 | } 443 | foreach ($Item in $Object) { 444 | if ($Item -is [IDictionary]) { $Object; Break } 445 | elseif ( $Null -eq $Keys ) { [ordered]@{ $ValueName = $Item } } 446 | else { 447 | $Dictionary = [ordered]@{} 448 | if ($Null -ne $Item) { 449 | foreach ($Key in @($Keys)) { 450 | if ($Null -eq $Key) { $Key = $ValueName } 451 | $Dictionary.Add($Key, $Item.PSObject.properties[$Key].Value) 452 | } 453 | } 454 | $Dictionary 455 | } 456 | } 457 | } 458 | } 459 | function SetExpression ($Key = '*', $Keys, $Expression) { 460 | $Wildcard = if ($Key -is [ScriptBlock]) { $Keys } else { 461 | if (-not $Keys.Contains($Key)) { 462 | if ($Key.Trim() -eq '*') { $Keys } 463 | else { 464 | $Side, $Asterisks = $Key.Split('.', 2) 465 | if ($Null -ne $Asterisks -and $Asterisks.Trim() -eq '*') { 466 | if ($Side -eq 'Left') { $LeftKeys } elseif ($Side -eq 'Right') { $RightKeys } 467 | } 468 | } 469 | } 470 | } 471 | if ($Null -ne $Wildcard) { 472 | if ($Null -eq $Expression) { $Expression = $Key } 473 | foreach ($Key in $Wildcard) { 474 | if ($Null -ne $Key -and -not $Expressions.Contains($Key)) { 475 | $Expressions[$Key] = $Expression 476 | } 477 | } 478 | } 479 | else { $Expressions[$Key] = if ($Expression) { $Expression } else { ' * ' } } 480 | } 481 | function Combine { 482 | param( 483 | [Parameter(ValueFromPipeLine = $True)]$Item, 484 | $Where, 485 | $Expressions, 486 | $Discern, 487 | $ValueName, 488 | $LeftRight, 489 | $RightLeft 490 | ) 491 | begin { 492 | if ($Where) { $Where = [ScriptBlock]::Create($Where) } # Pull into the current (module) scope 493 | } 494 | process { 495 | $Left = $Item.Left 496 | $Right = $Item.Right 497 | $LeftIndex = $Item.LeftIndex 498 | $RightIndex = $Item.RightIndex 499 | 500 | if (-not $Where -or (& $Where)) { 501 | $Nodes = [Ordered]@{} 502 | foreach ($Name in $Expressions.Get_Keys()) { 503 | $Tuple = 504 | if ($Expressions[$Name] -is [ScriptBlock]) { 505 | $ScriptBlock = $Expressions[$Name] 506 | @{ 0 = $Name.foreach($ScriptBlock)[0] } 507 | } 508 | else { 509 | $Key = $Expressions[$Name] 510 | if ($Left.Contains($Key) -or $Right.Contains($Key)) { 511 | if ($Left.Contains($Key) -and $Right.Contains($Key)) { @{ 0 = $Left[$Key]; 1 = $Right[$Key] } } 512 | elseif ($Left.Contains($Key)) { @{ 0 = $Left[$Key] } } 513 | else { @{ 0 = $Right[$Key] } } # if($Right.Contains($Name)) 514 | } 515 | elseif ($Key.Trim() -eq '*') { 516 | if ($LeftRight[$Name] -eq $Name) { 517 | if ($Null -ne $LeftIndex) { @{ 0 = $Left[$Name] } } else { @{ 0 = $Right[$Name] } } 518 | } 519 | elseif ($LeftRight.Contains($Name) -and $RightLeft.Contains($Name)) { 520 | @{ 0 = if ($Null -ne $LeftIndex) { $Left[$Name] }; 1 = if ($Null -ne $RightIndex) { $Right[$Name] } } 521 | } 522 | elseif ($RightLeft.Contains($Name)) { 523 | if ($Left.Contains($Name)) { @{ 0 = $Left[$Name] } } 524 | elseif ($null -ne $RightIndex) { @{ 0 = $Right[$Name] } } 525 | elseif ($Right.Contains($RightLeft[$Name])) { @{ 0 = $Left[$RightLeft[$Name]] } } #45 526 | } 527 | elseif ($LeftRight.Contains($Name)) { 528 | if($Right.Contains($Name)) { @{ 0 = $Right[$Name] } } 529 | elseif ($null -ne $LeftIndex) { @{ 0 = $Left[$Name] } } 530 | elseif ($Left.Contains($LeftRight[$Name])) { @{ 0 = $Right[$LeftRight[$Name]] } } #45 531 | } 532 | elseif (-not $Right.Contains($Name)) { @{ 0 = $Left[$Name] } } 533 | elseif (-not $Left.Contains($Name)) { @{ 0 = $Right[$Name] } } 534 | else { @{ 0 = $Left[$Name]; 1 = $Right[$Name] } } 535 | } 536 | else { 537 | $Side, $Key = $Key.Split('.', 2) 538 | if ($Null -ne $Key) { 539 | if ($Side[0] -eq 'L') { 540 | if ($Left.Contains($Key)) { @{ 0 = $Left[$Key] } } 541 | elseif ($Key -eq '*') { 542 | if ($Null -ne $LeftIndex -and $Left.Contains($Name)) { @{ 0 = $Left[$Name] } } 543 | elseif ($Null -ne $RightIndex -and $Right.Contains($Name)) { @{ 0 = $Right[$Name] } } 544 | } 545 | } 546 | if ($Side[0] -eq 'R') { 547 | if ($Right.Contains($Key)) { @{ 0 = $Right[$Key] } } 548 | elseif ($Key -eq '*') { 549 | if ($Null -ne $RightIndex -and $Right.Contains($Name)) { @{ 0 = $Right[$Name] } } 550 | elseif ($Null -ne $LeftIndex -and $Left.Contains($Name)) { @{ 0 = $Left[$Name] } } 551 | } 552 | } 553 | } else { Throw [ArgumentNullException]::new("The property '$Key' doesn't exists", $Key) } 554 | } 555 | } 556 | if ($Tuple -isnot [IDictionary] ) { $Node = $Null } 557 | elseif ($Tuple.Count -eq 1) { $Node = $Tuple[0] } 558 | else { 559 | $Node = [Collection[PSObject]]::new() 560 | if ($Tuple[0] -is [Collection[PSObject]]) { foreach ($Value in $Tuple[0]) { $Node.Add($Value) } } else { $Node.Add($Tuple[0]) } 561 | if ($Tuple[1] -is [Collection[PSObject]]) { foreach ($Value in $Tuple[1]) { $Node.Add($Value) } } else { $Node.Add($Tuple[1]) } 562 | } 563 | if ($Null -ne $Discern -and $Node -is [Collection[PSObject]]) { 564 | if ($Node.get_Count() -eq $Discern.Count + 1) { $Nodes[$Name] = $Node[$Node.get_Count() - $Discern.Count - 1] } 565 | if ($Node.get_Count() -gt $Discern.Count + 1) { $Nodes[$Name] = $Node[0..($Node.get_Count() - $Discern.Count - 1)] } 566 | for ($i = [math]::Min($Node.get_Count(), $Discern.Count); $i -gt 0; $i--) { 567 | $Rename = $Discern[$Discern.Count - $i] 568 | $Rename = if ($Rename.Contains('*')) { ([regex]"\*").Replace($Rename, $Name, 1) } elseif ($Name -eq $ValueName) { $Rename } else { $Rename + $Name } 569 | if (-not $Rename) { $Rename = $ValueName} 570 | $Nodes[$Rename] = if ($Nodes.Contains($Rename)) { @($Nodes[$Rename]) + $Node[$Node.get_Count() - $i] } else { $Node[$Node.get_Count() - $i] } 571 | } 572 | } elseif ($Null -ne $Discern -and $Name -eq $ValueName) { 573 | $Nodes[$Discern[0]] = $Node 574 | } else { 575 | $Nodes[$Name] = $Node 576 | } 577 | } 578 | if ($Nodes.get_Count()) { 579 | if ($Nodes.get_Count() -eq 1 -and $Nodes.Contains($ValueName)) { ,$Nodes[$ValueName] } # return scalar array 580 | else { [PSCustomObject]$Nodes } 581 | } 582 | } 583 | } 584 | } 585 | function ProcessObject { 586 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Discern', Justification = 'False positive as rule does not scan child scopes')] 587 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Where', Justification = 'False positive as rule does not scan child scopes')] 588 | param( 589 | $RightObject, 590 | [array]$On = @(), 591 | $Using, 592 | [array]$Equals = @(), 593 | $Discern, 594 | $Property, 595 | $Where, 596 | $JoinType, 597 | $ValueName, 598 | [switch]$Strict, 599 | [switch]$MatchCase, 600 | [Switch]$SkipSameIndex, 601 | [Parameter(ValueFromPipeLine = $True)]$LeftObject 602 | ) 603 | begin { 604 | $Expressions = [Ordered]@{} 605 | $StringComparer = if ($MatchCase) { [StringComparer]::Ordinal } Else { [StringComparer]::OrdinalIgnoreCase } 606 | $Keys, $LeftKeys, $RightKeys, $Pipeline, $LeftList, $AsDictionary = $Null 607 | $InnerRight = [HashSet[int]]::new() 608 | $RightIndices = [Dictionary[string, object]]::new($StringComparer) 609 | $LeftRight = @{}; $RightLeft = @{}; $LeftNull = [ordered]@{}; $RightNull = [ordered]@{} 610 | $LeftIndex = 0 611 | if ($RightObject -is [IDictionary]) { $RightList = @($RightObject) } 612 | else { 613 | $RightName = if ($Equals.Count -eq 0 -and $On.Count -eq 1 -and $On[0] -isnot [ScriptBlock] -and "$($On[0])".Trim() -ne '*') { $On[0] } 614 | elseif ($Equals.Count -eq 1 -and $Equals[0] -isnot [ScriptBlock] -and "$($Equals[0])".Trim() -ne '*') { $Equals[0] } else { $ValueName } 615 | $RightList = @(AsDictionary $RightObject -ValueName $RightName) 616 | } 617 | if ($RightList.Count) { $RightKeys = $RightList[0].get_Keys() } else { $RightKeys = @() } 618 | if ($Using) { $Using = [ScriptBlock]::Create($Using) } # Pull into the current (module) scope 619 | $Combine = $null 620 | } 621 | process { 622 | if (-not $AsDictionary) { 623 | $LeftName = if ($On.Count -eq 1 -and $On[0] -isnot [ScriptBlock] -and "$($On[0])".Trim() -ne '*') { $On[0] } else { $ValueName } 624 | $AsDictionary = { AsDictionary -ValueName $LeftName }.GetSteppablePipeline() 625 | $AsDictionary.Begin($True) 626 | } 627 | $Left = if ($LeftObject -is[IDictionary]) { $LeftObject } elseif ($Null -ne $LeftObject) { $AsDictionary.Process((,$LeftObject))[0] } 628 | if (-not $LeftKeys) { 629 | if ($Null -ne $Left) { $LeftKeys = $Left.get_Keys() } else { $LeftKeys = @() } 630 | $Keys = [HashSet[string]]::new([string[]](@($LeftKeys) + @($RightKeys)), [StringComparer]::InvariantCultureIgnoreCase) 631 | } 632 | if ($Null -eq $Combine) { 633 | if ($On.Count) { 634 | $OnWildCard = $On.Count -eq 1 -and "$($On[0])".Trim() -eq '*' # Use e.g. -On ' * ' if there exists an '*' property 635 | $EqualsWildCard = $Equals.Count -eq 1 -and "$($Equals[0])".Trim() -eq '*' 636 | if ($OnWildCard) { 637 | if ($Equals.Count -and -not $EqualsWildCard) { $On = $Equals } 638 | else { $On = $LeftKeys.Where{ $RightKeys -eq $_ } } 639 | } 640 | elseif ($EqualsWildCard) { $Equals = $On } 641 | if ($On.Count -gt $Equals.Count) { $Equals += $On[($Equals.Count)..($On.Count - 1)] } 642 | elseif ($On.Count -lt $Equals.Count) { $On += $Equals[($On.Count)..($Equals.Count - 1)] } 643 | if ($Null -ne $Left) { 644 | for ($i = 0; $i -lt $On.Count; $i++) { 645 | if ( $On[$i] -is [ScriptBlock] ) { if ( $On[$i] -Like '*$Right*' ) { Write-Warning 'Use the -Using parameter for comparison expressions' } } 646 | else { 647 | if ($On[$i] -notin $LeftKeys) { Throw [MissingMemberException]::new("`$LeftObject", $On[$i]) } 648 | $LeftRight[$On[$i]] = $Equals[$i] 649 | } 650 | if ( $Equals[$i] -is [ScriptBlock] ) { if ( $On[$i] -Like '*$Left*' ) { Write-Warning 'Use the -Using parameter for comparison expressions' } } 651 | else { 652 | if ($Equals[$i] -notin $RightKeys) { Throw [MissingMemberException]::new("`$RightObject", $Equals[$i]) } 653 | $RightLeft[$Equals[$i]] = $On[$i] 654 | } 655 | } 656 | } 657 | $RightIndex = 0 658 | foreach ($Right in $RightList) { 659 | $Dictionary = $RightIndices # $Dictionary references the $RightList 660 | $Count = $Equals.Count 661 | foreach ($Name in $Equals) { 662 | $Value = 663 | if ($Name -isnot [ScriptBlock]) { $Right[$Name] } 664 | elseif ($Right.get_Count() -eq 1 -and $Right.Contains($ValueName)) { $Right[$ValueName].foreach($Name) } 665 | else { $Right.foreach($Name)[0] } 666 | $Key = # WET performance: https://github.com/orgs/PowerShell/discussions/19322 667 | if ( $Null -eq $Value ) { "$Esc`$Null" } 668 | else { 669 | $Type = if ($Strict) { "$($Value.GetType())" } 670 | if ($Value -is [String]) { $Value } 671 | elseif ($Value -is [ValueType]) { "$Type$Value" } 672 | elseif ($Value -is [System.MarshalByRefObject]) { "$Esc$Type[$($Value | Select-Object *)]" } 673 | elseif ($Value -is [PSCustomObject]) { "$Esc$Type[$Value]" } 674 | elseif ($Value -is [IDictionary]) { "$Esc$Type{$($Value.GetEnumerator())}" } 675 | elseif ($Value -is [Array]) { "$Esc$Type($Value)" } 676 | else { "$Esc$Type$Value" } 677 | } 678 | if (-Not --$Count) { break } 679 | if (-not $Dictionary.ContainsKey($Key)) { $Dictionary[$Key] = [Dictionary[string, object]]::new($StringComparer) } 680 | $Dictionary = $Dictionary[$Key] 681 | } 682 | if ($Dictionary.ContainsKey($Key)) { $Dictionary[$Key].Add($RightIndex++) } 683 | else { $Dictionary[$Key] = [List[Int]]$RightIndex++ } 684 | } 685 | } 686 | foreach ($Key in $LeftKeys) { 687 | if ($Left[$Key] -isnot [Collection[PSObject]]) { $LeftNull[$Key] = $Null } 688 | else { $LeftNull[$Key] = [Collection[PSObject]]( ,$Null * $Left[$Key].Count) } 689 | } 690 | foreach ($Key in $RightKeys) { 691 | $RightNull[$Key] = if ($RightList) { 692 | if ($RightList[0][$Key] -isnot [Collection[PSObject]]) { $Null } 693 | else { [Collection[PSObject]]( ,$Null * $Left[$Key].Count) } 694 | } 695 | } 696 | if ($Property) { 697 | foreach ($Item in @($Property)) { 698 | if ($Item -is [IDictionary]) { foreach ($Key in $Item.Get_Keys()) { SetExpression -Key $Key -Keys $Keys -Expression $Item[$Key] } } 699 | else { SetExpression -Key $Item -Keys $Keys } 700 | } 701 | } else { SetExpression -Keys $Keys } 702 | foreach ($Key in @($Expressions.get_Keys())) { 703 | if ($Expressions[$Key] -is [ScriptBlock]) { $Expressions[$Key] = [scriptblock]::Create($Expressions[$Key]) } 704 | } 705 | $Combine = { Combine -LeftRight $LeftRight -RightLeft $RightLeft -Where $Where -Expression $Expressions -Discern $Discern -ValueName $ValueName }.GetSteppablePipeline() 706 | $Combine.Begin($True) 707 | } 708 | if ($Null -ne $Left) { 709 | $InnerLeft = $False 710 | $Indices = 711 | if ($On.Count) { 712 | $Dictionary = $RightIndices 713 | foreach ($Name in $On) { 714 | $Value = 715 | if ($Name -isnot [ScriptBlock]) { $Left[$Name] } 716 | elseif ($Left.get_Count() -eq 1 -and $Left.Contains($ValueName)) { $Left[$ValueName].foreach($Name)[0] } 717 | else { $Left.foreach($Name)[0] } 718 | $Key = # WET performance: https://github.com/orgs/PowerShell/discussions/19322 719 | if ( $Null -eq $Value ) { "$Esc`$Null" } 720 | else { 721 | $Type = if ($Strict) { "$($Value.GetType())" } 722 | if ($Value -is [String]) { $Value } 723 | elseif ($Value -is [ValueType]) { "$Type$Value" } 724 | elseif ($Value -is [System.MarshalByRefObject]) { "$Esc$Type[$($Value | Select-Object *)]" } 725 | elseif ($Value -is [PSCustomObject]) { "$Esc$Type[$Value]" } 726 | elseif ($Value -is [IDictionary]) { "$Esc$Type{$($Value.GetEnumerator())}" } 727 | elseif ($Value -is [Array]) { "$Esc$Type($Value)" } 728 | else { "$Esc$Type$Value" } } 729 | $Dictionary = if ($Dictionary.ContainsKey($Key)) { $Dictionary[$Key] } 730 | if ($Null -eq $Dictionary) { break } 731 | } 732 | if ($Null -ne $Dictionary) { $Dictionary } 733 | } 734 | elseif ($Using) { 735 | if ($JoinType -eq 'Cross') { Throw [ArgumentException]::new('Invalid cross join', 'Using') } 736 | for ($RightIndex = 0; $RightIndex -lt $RightList.Count; $RightIndex++) { 737 | $Right = $RightList[$RightIndex] 738 | if (& $Using) { $RightIndex } 739 | } 740 | } 741 | elseif ($JoinType -eq 'Cross') { if ($RightList) { 0..($RightList.Length - 1) } else { @() } } 742 | elseif ($LeftIndex -lt $RightList.Count) { $LeftIndex } else { $Null } 743 | foreach ($RightIndex in $Indices) { 744 | if ($SkipSameIndex -and $LeftIndex -eq $RightIndex) { 745 | $InnerLeft = $True 746 | $Null = $InnerRight.Add($RightIndex) 747 | } 748 | else { 749 | $Object = $Combine.Process(@{ Left = $Left; Right = $RightList[$RightIndex]; LeftIndex = $LeftIndex; RightIndex = $RightIndex }) 750 | if ($Null -ne $Object -and $Object.get_Count() -gt 0) { 751 | if ($JoinType -ne 'Outer') { $Object } 752 | $InnerLeft = $True 753 | $Null = $InnerRight.Add($RightIndex) 754 | } 755 | } 756 | } 757 | } 758 | else { 759 | $InnerLeft = $True 760 | for ($RightIndex = 0; $RightIndex -lt $RightList.Count; $RightIndex++) { 761 | if (-not $InnerRight.Contains($RightIndex)) { 762 | $Combine.Process(@{ Left = $LeftNull; Right = $RightList[$RightIndex]; LeftIndex = $Null; RightIndex = $RightIndex }) 763 | } 764 | } 765 | } 766 | if (-not $InnerLeft -and ($JoinType -in 'Left', 'Full', 'Outer')) { 767 | $Combine.Process(@{ Left = $Left; Right = $RightNull; LeftIndex = $LeftIndex; RightIndex = $Null }) 768 | } 769 | $LeftIndex++ 770 | } 771 | end { 772 | if ($AsDictionary) { $AsDictionary.End() } 773 | if($Combine) { $Combine.End() } 774 | } 775 | } 776 | 777 | $Parameters = [Dictionary[String,Object]]::new($PSBoundParameters) 778 | $Parameters['ValueName'] = $ValueName 779 | if ($Parameters.TryGetValue('OutBuffer', [ref]$Null)) { $Parameters['OutBuffer'] = 1 } 780 | if ($Parameters.ContainsKey('Discern') -and -not $Discern) { $Parameters['Discern'] = @() } 781 | if ($JoinType -eq 'Outer' -and -not $Parameters.ContainsKey('On')) { $Parameters['On'] = '*' } 782 | 783 | $LeftList, $Pipeline = $Null 784 | } 785 | 786 | process { 787 | # The Process block is invoked (once) if the pipeline is omitted but not if it is empty: @() 788 | # if ($Null -eq $LeftKeys) { $LeftKeys = GetKeys $LeftObject } 789 | 790 | trap { $PSCmdlet.ThrowTerminatingError($_) } 791 | if ($Null -eq $Pipeline) { 792 | if ($Null -ne $_ -and $Parameters.ContainsKey('RightObject')) { 793 | $Pipeline = { ProcessObject @Parameters }.GetSteppablePipeline() 794 | $Pipeline.Begin($PSCmdlet) 795 | } 796 | else { 797 | $Pipeline = $False 798 | $LeftList = [List[Object]]::New() 799 | } 800 | } 801 | if ($Pipeline) { $Pipeline.Process($_) } else { $LeftList.Add($_) } 802 | } 803 | end { 804 | trap { $PSCmdlet.ThrowTerminatingError($_) } 805 | if (!($Parameters.ContainsKey('LeftObject') -or $LeftList) -and -not $Parameters.ContainsKey('RightObject')) { 806 | Throw [ArgumentException]::new('A value for either the LeftObject, the pipeline, or the RightObject is required.') 807 | } 808 | if ($Pipeline -eq $False) { # Not yet streamed/processed 809 | if (-not $LeftList) { 810 | if ($Parameters.ContainsKey('LeftObject')) { 811 | $LeftList = $LeftObject 812 | } 813 | else { # Right self-join 814 | if ($Parameters.ContainsKey('On') -and -not $Parameters.ContainsKey('Equal')) { $Parameters['SkipSameIndex'] = $True } 815 | $LeftList = $RightObject 816 | } 817 | } 818 | if ($Parameters.ContainsKey('LeftObject')) { $Null = $Parameters.remove('LeftObject') } 819 | if (-not $Parameters.ContainsKey('RightObject')) { # Left self-join 820 | if ($Parameters.ContainsKey('On') -and -not $Parameters.ContainsKey('Equal')) { $Parameters['SkipSameIndex'] = $True } 821 | $Parameters['RightObject'] = $LeftList 822 | } 823 | $Pipeline = { ProcessObject @Parameters }.GetSteppablePipeline() 824 | $Pipeline.Begin($True) 825 | foreach ($Left in $LeftList) { $Pipeline.Process($Left) } 826 | } 827 | if ('Right', 'Full', 'Outer' -eq $JoinType) { 828 | if ($Null -eq $Pipeline) { 829 | if ($Parameters.ContainsKey('LeftObject')) { $Null = $Parameters.remove('LeftObject') } 830 | $Pipeline = { ProcessObject @Parameters }.GetSteppablePipeline() 831 | $PipeLine.Begin($True) 832 | } 833 | $Pipeline.Process($Null) 834 | } 835 | if ($Pipeline) { $Pipeline.End() } 836 | } 837 | }; Set-Alias Join Join-Object 838 | 839 | $JoinCommand = Get-Command Join-Object 840 | $MetaData = [CommandMetadata]$JoinCommand 841 | $ProxyCommand = [ProxyCommand]::Create($MetaData) 842 | $Ast = [Language.Parser]::ParseInput($ProxyCommand, [ref]$null, [ref]$null) 843 | $BeginOffset = $Ast.BeginBlock.Extent.StartOffset 844 | $ParamBlock = $Ast.Extent.Text.SubString(0, $BeginOffset) 845 | $ScriptBlock = $Ast.Extent.Text.SubString($BeginOffset) 846 | 847 | $Proxies = 848 | @{ Name = 'InnerJoin'; Alias = 'InnerJoin-Object'; Default = "JoinType = 'Inner'" }, # https://github.com/PowerShell/PowerShell/issues/25642 849 | @{ Name = 'LeftJoin'; Alias = 'LeftJoin-Object'; Default = "JoinType = 'Left'" }, 850 | @{ Name = 'RightJoin'; Alias = 'RightJoin-Object'; Default = "JoinType = 'Right'" }, 851 | @{ Name = 'FullJoin'; Alias = 'FullJoin-Object'; Default = "JoinType = 'Full'" }, 852 | @{ Name = 'OuterJoin'; Alias = 'OuterJoin-Object'; Default = "JoinType = 'Outer'" }, 853 | @{ Name = 'CrossJoin'; Alias = 'CrossJoin-Object'; Default = "JoinType = 'Cross'" }, 854 | @{ Name = 'Update-Object'; Alias = 'Update'; Default = "JoinType = 'Left'", "Property = @{ '*' = 'Right.*' }" }, 855 | @{ Name = 'Merge-Object'; Alias = 'Merge'; Default = "JoinType = 'Full'", "Property = @{ '*' = 'Right.*' }" }, 856 | @{ Name = 'Get-Difference'; Alias = 'Differs'; Default = "JoinType = 'Outer'", "Property = @{ '*' = 'Right.*' }" } 857 | 858 | $Function = [List[String]]'Join-Object' 859 | $Alias = [LIst[String]]'Join' 860 | 861 | foreach ($Proxy in $Proxies) { 862 | $ProxyCommand = @( 863 | $ParamBlock 864 | 'DynamicParam {' 865 | foreach ($Default in @($Proxy.Default)) { ' $PSBoundParameters.' + $Default } 866 | '}' 867 | $ScriptBlock 868 | ) -Join [Environment]::NewLine 869 | $Null = New-Item -Path Function:\ -Name $Proxy.Name -Value $ProxyCommand -Force 870 | Set-Alias $Proxy.Alias $Proxy.Name 871 | $Function.Add($Proxy.Name) 872 | $Alias.Add($Proxy.Alias) 873 | } 874 | 875 | Export-ModuleMember -Function $Function -Alias $Alias --------------------------------------------------------------------------------