Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Archives
Subscribe
December 23, 2025

Leaping to a PowerShell Solution

In this issue:

  • Getting Started
  • IsLeapYear
  • Finding Leap Year Files
  • Formatted Grouping
  • Creating an HTML Report
  • Bonus: SpectreConsole Integration
  • Summary

Time to solve the PowerShell scripting challenge from last month. I hope you took some time to figure this out. The process of solving the problem is more important than the solution.

I challenged you to find files in a give file that were last modified in a leap year and display them grouped by year. As a bonus, I suggested creating an HTML report of the results.

Here's how I approached the problem.

Getting Started

The first step might be to display files in a given directory including the year the file was last modified. We know that the file object has a LastWriteTime property which is a DateTime object. This means it has a Year property we can use.

PS C:\> $f = Get-Item $PROFILE
PS C:\> $f.LastWriteTime

Thursday, March 20, 2025 8:47:20 AM

PS C:\> $f.LastWriteTime.Year
2025

I might list files in a directory like this:

PS C:\> Get-ChildItem c:\temp\ -file | Select-Object -first 5 -Property Name,LastWriteTime,
@{Name="Year";Expression = {$_.LastWriteTime.Year}}

Name                LastWriteTime         Year
----                -------------         ----
01 Start Me Up.mp3  3/12/2023 3:57:58 PM  2023
01 Start Me Up.zip  5/28/2025 1:26:33 PM  2025
01 Tom Sawyer 1.zip 5/28/2025 1:26:34 PM  2025
2112.mp3            5/27/2009 1:04:43 PM  2009
40.txt              9/29/2025 10:49:47 AM 2025

IsLeapYear

Next, I have to determine if a year is a leap year. I could create a static list of known leap years and compare the date. I could try to test the year mathematically. But there is an even easier way. The .NET DateTime class has a static method called IsLeapYear that can help.

PS C:\> [datetime]::IsLeapYear(2025)
False
PS C:\> [datetime]::IsLeapYear(2024)
True

I can get a list of leap years from the last 25 years:

PS C:\> 2000..2025 | Where-Object {[Datetime]::IsLeapYear($_)}

2000
2004
2008
2012
2016
2020
2024

Finding Leap Year Files

With this concept in mind, I can now filter files in a directory to only those modified in a leap year.

PS C:\> Get-ChildItem c:\temp\ -file | Where-Object {[DateTime]::IsLeapYear($_.LastWriteTime.Year)}

    Directory: C:\temp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---           12/3/2020  4:33 PM           1729 a.png
-a---          10/16/2012  3:31 PM           1382 Demo-PowerShellHTML.ps1
-a---           10/3/2016  9:03 AM            500 Download-2016Eval.ps1
-a---            5/1/2020  8:31 AM            263 Get-WTProcess.ps1
-a---           3/10/2016  9:22 AM        3784538 Loituma - Ievas polka.mp3
-a---           4/25/2024 12:47 PM            281 multiline-prompt.ps1
-a---            3/1/2016  2:10 PM           5942 New-PSDriveHere.ps1
-a---           2/29/2024  4:44 PM          10060 PSRefresh.ps1
-a---           12/4/2024  4:07 PM          30125 psworkitem.md
-a---           9/15/2024  4:57 PM         104448 testef6.dll
-a---           1/22/2024  4:21 AM            965 testef6.dll.config
-a---           9/15/2024  4:57 PM          25592 testef6.pdb
-a---          10/29/2020  3:24 PM           7552 wtpsremote.ps1

Formatted Grouping

To group the results for display you might be tempted to jump to Group-Object. However, for a simple display, Format-Table will work, especially because it has a GroupBy parameter. The tricky part is that the value we want to group on must be calculated or derived from the file object. We can do this with a calculated property.

PS C:\> Get-ChildItem c:\temp\ -file |
Where-Object {[DateTime]::IsLeapYear($_.LastWriteTime.Year)} |
Sort-Object LastWriteTime,Name |
Format-Table -GroupBy @{Name="Year";Expression={$_.LastWriteTime.Year}} -Property Mode,LastWriteTime,Length,Name

   Year: 2012

Mode  LastWriteTime         Length Name
----  -------------         ------ ----
-a--- 10/16/2012 3:31:46 PM   1382 Demo-PowerShellHTML.ps1

   Year: 2016

Mode  LastWriteTime         Length Name
----  -------------         ------ ----
-a--- 3/1/2016 2:10:23 PM     5942 New-PSDriveHere.ps1
-a--- 3/10/2016 9:22:17 AM 3784538 Loituma - Ievas polka.mp3
-a--- 10/3/2016 9:03:59 AM     500 Download-2016Eval.ps1

   Year: 2020

Mode  LastWriteTime         Length Name
----  -------------         ------ ----
-a--- 5/1/2020 8:31:14 AM      263 Get-WTProcess.ps1
-a--- 10/29/2020 3:24:53 PM   7552 wtpsremote.ps1
-a--- 12/3/2020 4:33:20 PM    1729 a.png

   Year: 2024

Mode  LastWriteTime         Length Name
----  -------------         ------ ----
-a--- 1/22/2024 4:21:34 AM     965 testef6.dll.config
-a--- 2/29/2024 4:44:33 PM   10060 PSRefresh.ps1
-a--- 4/25/2024 12:47:38 PM    281 multiline-prompt.ps1
-a--- 9/15/2024 4:57:47 PM  104448 testef6.dll
-a--- 9/15/2024 4:57:47 PM   25592 testef6.pdb
-a--- 12/4/2024 4:07:30 PM   30125 psworkitem.md

There are a few caveats with this approach. First, you must sort the objects before formatting. I am sorting first on the LastWriteTime and then on the Name to get a consistent order. The secondary sort is probably not necessarily because it will only be used with files that have identical LastWriteTime values.

Second, the value for the GroupBy parameter can be a script block or hashtable. I recommend using the hashtable form as I'm doing so that you have a clean label for the grouping. Finally, when using format grouping, you must also specify the properties you want to see. In my solution, I am simply repeating the default properties shown by Get-ChildItem.

Creating an HTML Report

Next, let's move on and use this data to create an HTML report. The following code would go in a script file which I could then execute

#the path to search
$Path = "C:\Temp"
#the output file
$htmlPath = "C:\Work\LeapYearFiles.html"

#get the files
$files = Get-ChildItem $Path -file |
Where-Object {[DateTime]::IsLeapYear($_.LastWriteTime.Year)} |
Sort-Object LastWriteTime,Name

I should have logic to test if there are any files.

if ($files.count -gt 0 ) {
    ##code
}
else {
    Write-Warning "No matching files found in $Path"
}

If files are found, I can create the HTML report. For this task, I want to use Group-Object and group on a custom property.

$grouped = $files | Group-Object {$_.LastWriteTime.Year}

This will create output like this:

Count Name                      Group
----- ----                      -----
    1 2012                      {C:\temp\Demo-PowerShellHTML.ps1}
    3 2016                      {C:\temp\New-PSDriveHere.ps1, C:\temp\Loituma - Ievas polka.mp3, …
    3 2020                      {C:\temp\Get-WTProcess.ps1, C:\temp\wtpsremote.ps1, C:\temp\a.png}
    6 2024                      {C:\temp\testef6.dll.config, C:\temp\PSRefresh.ps1, C:\temp\mult…

My plan is to process each group and create a section in the HTML report for each year. First, I'll define the style I want to use and a few other HTML basics

    $title = "Leap Year Files"
    $head = @"
<style>
body { background-color:#E5E4E2;
font-family:Monospace;
font-size:12pt; }
td, th { border:0px solid black;
border-collapse:collapse;
white-space:pre; }
th { color:white;
background-color:black; }
table, tr, td, th { padding: 5px; margin: 0px ;white-space:pre; }
table { margin-left:25px; width:50%;}
h2 {
    font-family:Tahoma;
    color:#6D7B8D;
    }
    .footer
    { color:green;
    margin-left:25px;
    font-family:Tahoma;
    font-size:10pt;
    }
    </style>
    <title>$Title</title>
"@

Instead of referencing an external CSS file, I am embedding the style directly in the HTML head section. Next, I'll create the basic HTML structure using HTML fragments.

#initialize a list to hold the fragments
$fragments = [System.Collections.Generic.List[string]]::New()
#add a header
$fragments.Add("<H1>Leap Year Files: $Path</H1>")
Foreach ($item in $grouped) {
    #insert a header for the year also showing the count of files
    $fragments.Add("<H2>$($item.Name) --> $($item.Count)</H2>")
    #create a table fragment for the grouping
    $table = $item.Group | Select-Object -Property Mode,LastWriteTime,
    @{Name="Size";Expression = {$_.Length}},Name |
    ConvertTo-Html -Fragment -As Table
    #add the table fragment to the list
    $fragments.Add($table)
}

It isn't a requirement, but I like adding a metadata footer to my HTML reports.

#add a footer
$footer = "<H5 class=footer>Report Date $(Get-Date)</H5>"
$fragments.add($footer)

My style sheet has a style defined for the footer class.

Finally, I can combine all the fragments into a single HTML document and save it to a file.

ConvertTo-HTML -Body $fragments -head $head | Out-File -FilePath $htmlPath
HTML Leap Year Report
figure 1

Bonus: SpectreConsole Integration

Since we spent some time this month exploring ways to to use the pwshSpectreConsole module, I thought it might be fun to integrate into this challenge. I'm going to start with grouped files.

$Path = "C:\Temp"
$files = Get-ChildItem $Path -file |
Where-Object {[DateTime]::IsLeapYear($_.LastWriteTime.Year)} |
Sort-Object LastWriteTime,Name | Group-Object {$_.LastWriteTime.Year}

My plan is to take each grouping and display the files as a Spectre table. I'll also use a rule line to set off the output.

#parameter hashtable for Format-SpectreTable
$fst = @{
    Title       = $null
    Color       = 'gold1'
    HeaderColor = 'Chartreuse1'
    Border      = 'HeavyEdge'
}
#parameter hashtable for Write-SpectreRule
$rule = @{
    Title     = ":calendar: Leap Year Files - $Path"
    Color     = $fst.HeaderColor
    LineColor = $fst.color
}

Write-SpectreRule @rule

foreach ($item in $files) {
    #set the table title
    $fst["Title"] = "$($item.Name) - $($item.count) files"
    $item.Group | Format-SpectreTable @fst | Format-SpectrePadded -Padding 1
}

Write-SpectreHost "[green italic] Report run: $(Get-Date)[/]" -PassThru
Formatted with SpectreConsole
figure 2

Summary

I hope you see a pattern here. Break the problem down into smaller pieces. Solve each piece individually. Then combine the pieces into a complete solution. Or put another way, get the data, then decide how to present it. As long as you think "objects", this will be a much easier process.

I hope you find these scripting challenges useful because I'll have another one very soon!

(c) 2022-2025 JDH Information Technology Solutions, Inc. - all rights reserved
Don't miss what's next. Subscribe to Behind the PowerShell Pipeline:

Add a comment:

Share this email:
Share on Facebook Share on LinkedIn Share on Threads Share on Reddit Share via email Share on Mastodon Share on Bluesky
GitHub
Bluesky
LinkedIn
Mastodon
https://jdhitso...
Powered by Buttondown, the easiest way to start and grow your newsletter.