1

I'm looking for some advice on filtering a multidimensional array and whether there are better ways over my existing approach.

I am filtering a multidimensional array where multiple values could be Null, "", " " etc. (i.e. they don't have what I've determined as valid values). The array looks like this:

$Files.Path
$Files.Owner
$Files.Vendor
$Files.Company
$Files.Product
$Files.Description
$Files.ProductVersion
$Files.FileVersion

To filter on Vendor, Company, Product, Description, I could do this a couple ways:

Method 1:

$Values = @(" ", "", $Null)
$NoMetadata = $Files | Where-Object {
    ($Values -contains $_.Vendor) -and `
    ($Values -contains $_.Company) -and `
    ($Values -contains $_.Product) -and `
    ($Values -contains $_.Description)
}

Method 2:

$NoMetadata = $Files | Where-Object { $_.Vendor -le 1 -and `
$_.Company -le 1 -and $_.Product -le 1 -and $_.Description -le 1 }

I appreciate any advice on improving my approach.

2 Answers 2

2

I suggest defining a helper function (using the simpler -le 1 conditional from your Method 2, which returns $True for $null, "" and " " alike):

Function test-NoMetaData {
  param([object] $obj)
  # Loop over all property names and inspect that property's value on the input object.
  foreach ($propName in 'Vendor', 'Company', 'Product', 'Description') {
    if ($obj.$propName -le 1) { return $False }
  }
  return $True
}

# Filter $Files down to those objects that lack the (complete) metadata.
$filesWithoutMetaData = $Files | Where-Object { test-NoMetaData $_ }

You could also place the code directly in the Where-Object block and refer to $_ directly.

Optional reading: If you want to make the function more sophisticated, read on.


Consider implementing a Filter function that you can use directly in the pipeline:

Filter select-WithMetaData {
  param([switch] $NotPresent) #  To invert the logic
  if ($Args) { Throw "Unrecognized arguments: $Args" }
  if (-not $MyInvocation.ExpectingInput) { return } # no pipeline input; nothing to do.
  $haveAllMetaData = $True
  foreach ($propName in 'Vendor', 'Company', 'Product', 'Description') {
    if ($_.$propName -le 1) { $haveAllMetaData = $False; break }
  }
  # Pass the input object through only if it has/doesn't have the requisite metadata.
  if ($haveAllMetaData -ne $NotPresent) { $_ }
}

$filesWithoutMetaData = $Files | select-WithMetaData -NotPresent
$filesWithMetaData =    $Files | select-WithMetaData

Filters are simplified functions that make it easier to define functionality that only accepts pipeline input: the body of the Filter function is invoked for each input object and $_ refers to that input object.

Filter functions are convenient, but have down-sides:

  • You cannot pass input as direct arguments as an alternative to pipeline input (unless you explicitly define a pipeline-binding parameter, which nullifies the advantages of the simplified syntax that Filter offers).
  • You cannot run initialization / cleanup code before / after pipeline input is received.

Use Function syntax to avoid these limitations - see below.


To write a function that alternatively accepts direct argument input and supports common parameters (which makes it an advanced function (cmdlet-like)), you must use the Function construct and explicitly declare a parameter as accepting pipeline input.

Additionally, your function must have a process { ... } block, which is invoked for each input item; optionally, it can have a begin {...} and an end { ... } block for pre-pipeline-input initialization / post-pipeline-input cleanup.
Caveat: If you do not use a process block, your function is only invoked once, at which point the pipeline-binding parameter variable only contains the last input object.

PSv3+ syntax:

Function Select-WithMetaData {
  [CmdletBinding()] # Make this an advanced function with common-parameter support.
  param(
    # Declare -File as accepting a single file directly or multiple files via the pipeline.
    [Parameter(ValueFromPipeline, Mandatory)] [object] $File,
    [switch] $NotPresent
  )

  # Invoked once with a directly passed -File argument bound to $File,
  # and for each input object, also bound to $File, if used in the pipeline.
  process { 
    $haveAllMetaData = $True
    foreach ($propName in 'Vendor', 'Company', 'Product', 'Description') {
      if ($File.$propName -le 1) { $haveAllMetaData = $False; break }
    }
    if ($haveAllMetaData -ne $NotPresent) { $File }
  }

}
Sign up to request clarification or add additional context in comments.

Comments

-1

Have modified the function slightly and this looks to be working well. Thanks to @mklement0

Function Test-AcMetadata {
    [CmdletBinding(SupportsShouldProcess = $False)]
    [OutputType([Bool])]
    Param (
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $False)]
        [object]$obj
    )
    ForEach ($Property in 'Vendor', 'Company', 'Product', 'Description') {
        If ($obj.$Property -ge 2) { Return $True }
    }
    Return $False
}

Which enables filtering for metadata or no metadata:

$NoMetadata = $Files | Where-Object { (Test-AcMetadata $_) -eq $False }
$Metadata = $Files | Where-Object { Test-AcMetadata $_ }

2 Comments

I am glad you found a solution, but your modifications to my approach are largely incidental to solving your core problem as asked, so in effect you've created a duplicate with distracting elements. Also note that in your example invocations you're (a) not taking advantage of the pipeline support and that (b) it wouldn't work, because as written your function would only ever process the last object in the pipeline. It's fine to demonstrate additional features and good practices in an answer, but I suggest doing that separately, as I've tried to do in my since-updated answer.
@mklement0 your answer is great! It's just ashamed the OP did not take it to heart. You can only lead them to water its up to them to drink.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.