Finding matches in arrays of objects in Powershell

Tom H picture Tom H · Aug 4, 2010 · Viewed 37.1k times · Source

I'm using custom objects to hold the name and schema from a set of SQL Server objects. I put the objects into an array, then I get another set of objects and put those into another array. What I'd like to do now is find all exact matches between the two arrays.

I'm currently using this:

$filteredSQLObjects = @()

foreach ($SQLObject1 in $SQLObjects1)
{
    foreach ($SQLObject2 in $SQLObjects2)
    {
        if ($SQLObject1.Name   -eq $SQLObject2.Name -and
            $SQLObject1.Schema -eq $SQLObject2.Schema)
        {
            $filteredSQLObjects += $SQLObject1
        }
    }
}

Is there a better/faster/cleaner way to do this? Originally when I was just working with arrays of strings I could just loop through one of the arrays and use -contains on the second, but with objects that doesn't seem possible.

Thanks!

Answer

George Howarth picture George Howarth · Aug 4, 2010

I think its better if you define the equality condition in an IsEqualTo method on your custom object. So something like this:

$myObject = New-Object PSObject
$myObject | Add-Member -MemberType NoteProperty -Name Name -Value $name
$myObject | Add-Member -MemberType NoteProperty -Name Schema -Value $schema
$myObject | Add-Member -MemberType ScriptMethod -Name IsEqualTo -Value {
    param (
        [PSObject]$Object
    )

    return (($this.Name -eq $Object.Name) -and ($this.Schema -eq $Object.Schema))
}

Then you can either do a one-liner like Keith showed us, or just do your double foreach iteration. Whichever you think is more readable:

$filteredSQLObjects = $SQLObjects1 | Where-Object { $SQLObject1 = $_; $SQLObjects2 | Where-Object { $_.IsEqualTo($SQLOBject1) } }

foreach ($SQLObject1 in $SQLObjects1)
{
    foreach ($SQLObject2 in $SQLObjects2)
    {
        if ($SQLObject1.IsEqualTo($SQLObject2))
        {
            $filteredSQLObjects += $SQLObject1
        }
    }
}

EDIT

OK, for a start, you can't add an Equals member because it already exists on System.Object (doh!). So I guess IsEqualTo will have to do instead.

What you can do is define your own function called Intersect-Object (the equivalent of .NET's Enumerable.Intersect method) which accepts pipeline input and returns the set intersection of two sequences (the ones that appear in both sequences). Be aware that I haven't fully-implemented this function (assumes each item in the collection specified by Sequence has an IsEqualTo method, doesn't check for duplicates before adding to $filteredSequence etc), but I hope you get the idea.

function Intersect-Object
{
    param (
        [Parameter(ValueFromPipeline = $true)]
        [PSObject]$Object,
        [Parameter(Mandatory = $true)]
        [PSObject[]]$Sequence
    )

    begin
    {
        $filteredSequence = @()
    }

    process
    {
        $Sequence | Where-Object { $_.IsEqualTo($Object) } | ForEach-Object { $filteredSequence += $_ }
    }

    end
    {
        return $filteredSequence
    }
}

Then your double foreach loop turns into this:

$filteredSQLObjects = $SQLObjects1 | Intersect-Object -Sequence $SQLObjects2