Measure multiple properties?

Viewed 580

I'd like to query a directory with Get-ChildItem and create a table with columns like Path, Size(in Gb), MinimumCreationTime, MaximumCreationTime. In foreach cycle I wrote 3 Measure commands. Is it possible Measure multiple properties with one command?

$pathes = @'
C:\open
C:\games
'@.Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries)

foreach ($path in $pathes){
    Get-ChildItem $path -Recurse | Measure Length -Sum 
    Get-ChildItem $path -Recurse | Measure CreationTime -Minimum
    Get-ChildItem $path -Recurse | Measure CreationTime -Maximum
    }
2 Answers

Is it possible Measure multiple properties with one command?

Yes you can - just not the way you need it to.

The reason we can't quite consolidate all three invocations here is that Measure-Object will perform all requested calculations on each input property - and it can't meaningfully calculate the sum of multiple [datetime] values.

So at best, we can do it with only 2 rounds of piping to Measure-Object:

$paths = 'C:\open','C:\games'

foreach ($path in $paths){
    # Let's call Get-ChildItem ONCE, and store the result
    $children = Get-ChildItem $path -Recurse 

    # Now let's do our calculations
    $fileSize = $children | Measure Length -Sum 
    $newAndOld = $children | Measure CreationTime -Minimum -Maximum

    # And finally create a new object to hold the details we calculated above
    [pscustomobject]@{
      Path           = $path
      TotalSize      = $fileSize.Sum
      OldestFileTime = $newAndOld.Minimum
      NewestFileTime = $newAndOld.Maximum
    }
}

PowerShell will default to table-format when displaying objects with 4 or fewer properties, so you can expect the default output in the shell to be something like this (depending on locale):

Path      TotalSize OldestFileTime       NewestFileTime
----      --------- --------------       --------------
C:\open  1234567890 1/1/2018 2:00:00 AM  1/31/2021 7:45:00 PM
C:\games 1234567890 1/1/2018 2:00:00 AM  1/31/2021 7:45:00 PM

It is possible with a single call to the Measure-Object command, using a calculated property that converts the CreationTime property to numeric type. Now -Sum can work with that (albeit we'll discard the sum for CreationTime).

After we've calculated the stats, we convert back to [DateTime] to get meaningful display values.

Since PS 7+, a calculated property can be used as Measure-Object argument. For older PS versions, we can use Select-Object to create a calculated property.

PS 7+ solution

foreach ($path in $pathes){

    $stats = Get-ChildItem $path -File -Recurse | 
             Measure-Object 'Length', { $_.CreationTime.Ticks } -Sum -Minimum -Maximum

    # Create the output for one table row
    [PSCustomObject]@{
        Path                = $path
        'Size(GB)'          = [math]::Round( $stats[0].Sum / 1GB, 2 )   # 2 = number of digits
        MinimumCreationTime = [DateTime] [Int64] $stats[1].Minimum
        MaximumCreationTime = [DateTime] [Int64] $stats[1].Maximum
    }
}

Explanation:

  • We are specifying two properties in the call to Measure-Object:
    • 1st property is just Length
    • 2nd property is a calculated property, meaning it gets its value from running a small script block. The script block converts the CreationTime to Int64 and uses it as value that will be measured.
  • When specifying multiple properties for Measure-Object, it outputs an array that contains an object for each property, which contains the stats.
    • $stats[0] contains the Sum, Minimum and Maximum for the Length property, of which we only take the Sum.
    • $stats[1] contains the Sum, Minimum and Maximum for the CreationTime property, of which we only take the Minimum and Maximum. Note that Measure-Object produces output of type [double], so we first have to convert back to [Int64] before finally converting back to [DateTime].

PS 5 solution

foreach ($path in $pathes){

    $stats = Get-ChildItem $path -File -Force | 
        Select-Object Length, @{ name = 'CreationTimeTicks'; expression = { $_.CreationTime.Ticks } } | 
        Measure-Object Length, CreationTimeTicks -Sum -Minimum -Maximum

    # Create the output for one table row - identical to PS 7+ solution
    [PSCustomObject]@{
        Path                = $path
        'Size(GB)'          = [math]::Round( $stats[0].Sum / 1GB, 2 )   # 2 = number of digits
        MinimumCreationTime = [DateTime] [Int64] $stats[1].Minimum
        MaximumCreationTime = [DateTime] [Int64] $stats[1].Maximum
    }
}

Explanation:

This is similar to the PS 7+ solution, except that we use Select-Object to create a calculated property named CreationTimeTicks, so we can pass it by name to the Measure-Object call.

Conclusion

While this code appears to work, the code presented by this answer is conceptually much clearer, so I would go with it.

Related