Get the Nth Working Day of the Month

Summary

I recently needed to schedule a task with Windows Task Scheduler to run at midday on the 5th weekday (Mon-Fri) of every month in 2020. Windows Task Scheduler lacks the functionality to configure such a schedule, so I needed to create a separate trigger for each month. Rather than do this by hand, I wrote a function in PowerShell to calculate the Nth weekday of a given month and year.

Function: Get-NthWorkingDayOfMonth

The function I wrote can be found here.

Usage

PS C:\> . .\Get-NthWorkingDayOfMonth.ps1
PS C:\> Get-NthWorkingDayOfMonth -Nth 10 -Month 1 -Year 2020

14 January 2020 00:00:00

For further details, see Get-NthWorkingDayOfMonth.

Caveats

When using this function there are a couple of caveats to consider…

If an Nth value is specified in combination with a month and year which cannot be satisfied then a terminating error is thrown, EG:

PS C:\Bitbucket> Get-NthWorkingDayOfMonth -Nth 25 -Month 1 -Year 2020
There isn't a 25th working day (Monday, Tuesday, Wednesday, Thursday, Friday) in January 2020.
At C:\Bitbucket\Get-NthWorkingDayOfMonth.ps1:112 char:5
+     Throw "There isn't a $($Nth)$($OrdinalIndicator) working day ($($ ...
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (There isn't a 2...n January 2020.:String) [], RuntimeException
    + FullyQualifiedErrorId : There isn't a 25th working day (Monday, Tuesday, Wednesday, Thursday, Friday) in January 2020.
 
PS C:\Bitbucket>

Depending on the use case, this may be acceptable. However, if used as part of a process which is expected to run every month then the caller would need to be written defensively to handle this situation and take some form of remedial action.

The other caveat is that it doesn’t take into consideration public holidays. Again, whether this is a concern or not is dependant on the use case.

I suppose this is why Windows Task Scheduler doesn’t provide the functionality to configure such a schedule.

Demonstration: My Use Case

Due to the caveats mentioned above, I’m not entirely sure how wise it is to use this function as part of an unattended process where the expectation is that it is guaranteed to run once a month. With that said, you may now be wondering what practical use this function has, so allow me to explain my use case…

As mentioned in the summary, I needed to schedule a task using Windows Task Scheduler to run on the 5th weekday (Mon-Fri) of every month in 2020. I started by using my function to produce a list of dates. Obtaining the 5th working day of each month where a working week consists of the days Mon-Fri is not an outlandish requirement, so I didn’t expect any errors to be returned but best to be safe…

Once satisfied there was nothing awry with the list of dates, my plan was to populate a scheduled task with a series of twelve date/time type triggers, one for each month. In my case, the scheduled task already existed, I just needed to replace the existing triggers with new date/times for 2020. I opted to do the following:

. .\Get-NthWorkingDayOfMonth.ps1

1..12 | ForEach-Object {
    $Date = Get-Date -Date (Get-NthWorkingDayOfMonth -Nth 5 -Month $_ -Year 2020) -Format 'yyyy-MM-dd'
    $Time = "T12:00:00"
    "    <TimeTrigger>"
    "      <StartBoundary>$($Date)$($Time)</StartBoundary>"
    "      <Enabled>true</Enabled>"
    "    </TimeTrigger>"
}  
schtasks.exe /create /xml "C:\thecliguy\Monthly Data Import Task.xml" /tn "TheCliGuy\Monthly Data Import Task" /ru "testdomain\thecliguy" /rp *

An alternative (and in hindsight simpler) approach would have been to use PowerShell’s New-ScheduledTaskTrigger cmdlet to replace the existing triggers with twelve new triggers:

. .\Get-NthWorkingDayOfMonth.ps1

$Triggers = @()
$Hour = 12

1..12 | ForEach-Object {
    $Date = Get-Date -Date (Get-NthWorkingDayOfMonth -Nth 5 -Month $_ -Year 2020) 
    
    # The New-ScheduledTaskTrigger cmdlet always results in the trigger setting
    # 'Synchronize across time zones' being enabled.
    # 
    # To workaround this, New-ScheduledTaskTrigger is supplied with a dummy 
    # date/time, which is subsequently overwritten by updating the trigger's
    # StartBoundary property with the actual date/time (formatted without a UTC 
    # offset).
    #
    # For further details see: 
    # https://www.thecliguy.co.uk/2020/02/09/scheduled-task-trigger-synchronize-across-time-zones/
    $Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date -Day 1 -Month 1 -Year 1 -Hour 0)
    $Trigger.StartBoundary = (Get-Date -Date $Date.Date -Hour $Hour -Format 'yyyy-MM-ddTHH:mm:ss')
    
    $Triggers += $Trigger
}

Set-ScheduledTask -TaskName "Monthly Data Import Task" -Trigger $Triggers

Finally, to ensure that the scheduled task had been correctly populated with the triggers, I used the following:

PS C:\> (Get-ScheduledTask -TaskName "Monthly Data Import Task").Triggers | Select-Object StartBoundary, Enabled

StartBoundary       Enabled
-------------       -------
2020-01-07T12:00:00    True
2020-02-07T12:00:00    True
2020-03-06T12:00:00    True
2020-04-07T12:00:00    True
2020-05-07T12:00:00    True
2020-06-05T12:00:00    True
2020-07-07T12:00:00    True
2020-08-07T12:00:00    True
2020-09-07T12:00:00    True
2020-10-07T12:00:00    True
2020-11-06T12:00:00    True
2020-12-07T12:00:00    True

Further Reading

Working on this task lead me to dabble with various scheduling tools and specifications to find out how they handled different conditions. These are just some of my notes pertaining to this research.

iCalendar

In my function Get-NthWorkingDayOfMonth I took the decision to throw a terminating error if an Nth value is specified in combination with a month and year which cannot be satisfied. I was curious to see how this problem is handled by the iCalendar specification.

The specification’s latest (as at Jan 2020) RFC (5545) acknowledges this issue as follows (excerpt from page 43):

Recurrence rules may generate recurrence instances with an invalid
date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
on a day where the local time is moved forward by an hour at 1:00
AM).  Such recurrence instances MUST be ignored and MUST NOT be
counted as part of the recurrence set.

Microsoft Outlook

The Outlook 2016 UI prevents a user from scheduling a recurrent event on a specific Nth weekday (Mon-Fri) of every month beyond the fourth:

Outlook 2016 Appointment Recurrence

I wondered if you could circumvent this limitation by scheduling a recurrent event on the 4th weekday of every month, saving it to an iCalendar file (.ics), editing the file in a text editor, replacing BYSETPOS=4 with BYSETPOS=5 and importing the file. The answer is no, you can’t. When attempting to import the file nothing happens, there’s no error message. I also tried by constructing a minimal iCalendar file (opposed to the one produced by Outlook), this also failed to import.

0 Comments

Post a comment

All comments are held for moderation.
Your email address will not be published. Required fields are marked *

Identification
Email
Name *
Website

Copyright © 2018 - 2020 thecliguy.co.uk
For details, see Licences and Copyright