Skip to content

Instantly share code, notes, and snippets.

@reijoh
Forked from Badgerati/Test-CronExpression.ps1
Last active June 14, 2022 18:59
Show Gist options
  • Save reijoh/fffa5642272fdc84d4b7a01691e91795 to your computer and use it in GitHub Desktop.
Save reijoh/fffa5642272fdc84d4b7a01691e91795 to your computer and use it in GitHub Desktop.
PowerShell cron expression parser, to check if a date/time matches a cron expression
<#
.DESCRIPTION
PowerShell cron expression parser, to check if a date/time matches a cron expression
Format:
<min> <hour> <day-of-month> <month> <day-of-week>
.PARAMETER Expression
A cron expression to validate
.PARAMETER DateTime
[Optional] A specific date/time to check cron expression against. (Default: DateTime.Now)
.EXAMPLE Test expression against the current date/time
Test-CronExpression -Expression '5/7 * 29 FEB,MAR *'
.EXAMPLE Test expression against a specific date/time
Test-CronExpression -Expression '5/7 * 29 FEB,MAR *' -DateTime ([DateTime]::Now)
#>
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Expression,
[Parameter(Mandatory=$false)]
[ValidateNotNullOrEmpty()]
[datetime]
$DateTime = [datetime]::Now
)
function Get-CronFields
{
return @(
'Minute',
'Hour',
'DayOfMonth',
'Month',
'DayOfWeek'
)
}
function Get-CronFieldConstraints
{
return @{
'MinMax' = @(
@(0, 59),
@(0, 23),
@(1, 31),
@(1, 12),
@(0, 6)
);
'DaysInMonths' = @(
31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
);
'Months' = @(
'January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'
)
}
}
function Get-CronPredefined
{
return @{
# normal
'@minutely' = '* * * * *';
'@hourly' = '0 * * * *';
'@daily' = '0 0 * * *';
'@weekly' = '0 0 * * 0';
'@monthly' = '0 0 1 * *';
'@quarterly' = '0 0 1 1,4,7,10';
'@yearly' = '0 0 1 1 *';
'@annually' = '0 0 1 1 *';
# twice
'@semihourly' = '0,30 * * * *';
'@semidaily' = '0 0,12 * * *';
'@semiweekly' = '0 0 * * 0,4';
'@semimonthly' = '0 0 1,15 * *';
'@semiyearly' = '0 0 1 1,6 *';
'@semiannually' = '0 0 1 1,6 *';
}
}
function Get-CronFieldAliases
{
return @{
'Month' = @{
'Jan' = 1;
'Feb' = 2;
'Mar' = 3;
'Apr' = 4;
'May' = 5;
'Jun' = 6;
'Jul' = 7;
'Aug' = 8;
'Sep' = 9;
'Oct' = 10;
'Nov' = 11;
'Dec' = 12;
};
'DayOfWeek' = @{
'Sun' = 0;
'Mon' = 1;
'Tue' = 2;
'Wed' = 3;
'Thu' = 4;
'Fri' = 5;
'Sat' = 6;
};
}
}
function ConvertFrom-CronExpression
{
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Expression
)
$Expression = $Expression.Trim()
# check predefineds
$predef = Get-CronPredefined
if ($null -ne $predef[$Expression]) {
$Expression = $predef[$Expression]
}
# split and check atoms length
$atoms = @($Expression -isplit '\s+')
if ($atoms.Length -ne 5) {
throw "Cron expression should only consist of 5 parts: $($Expression)"
}
# basic variables
$aliasRgx = '(?<tag>[a-z]{3})'
# get cron obj and validate atoms
$fields = Get-CronFields
$constraints = Get-CronFieldConstraints
$aliases = Get-CronFieldAliases
$cron = @{}
for ($i = 0; $i -lt $atoms.Length; $i++)
{
$_atomValues = @($atoms[$i] -split ',').Trim();
foreach ($_atom in $_atomValues)
{
$_field = $fields[$i]
$_constraint = $constraints.MinMax[$i]
$_aliases = $aliases[$_field]
# replace day of week and months with numbers
if ( @('month', 'dayofweek') -icontains $_field -and $_atom -imatch $aliasRgx )
{
$_aliasValue = $_aliases[$Matches['tag']]
if ($null -eq $_aliasValue) {
throw "Invalid $($_field) alias found: $($Matches['tag'])"
}
$_atom = $_aliasValue
}
# ensure atom is a valid value
if (!($_atom -imatch '^[\d|/|*|\-|,]+$')) {
throw "Invalid atom character: $($_atom)"
}
# replace * with min/max constraint
$_atom = $_atom -ireplace '\*', ($_constraint -join '-')
# parse the atom for either a literal, range, array, or interval
# literal
if ($_atom -imatch '^\d+$') {
$cron[$_field] += @([int]$_atom)
}
# range
elseif ($_atom -imatch '^(?<min>\d+)\-(?<max>\d+)$') {
$min = [int]($Matches['min'].Trim())
$max = [int]($Matches['max'].Trim())
# ensure min value is lower than max value
if ($min -gt $max) {
throw "Min value for $($_field) should not be greater than the max value"
}
# ensure range is not outside of constraint
if ($min -lt $_constraint[0]) {
$min = $_constraint[0]
}
if ($max -gt $_constraint[1]) {
$max = $_constraint[1]
}
# add entire range
$cron[$_field] += @($min..$max)
}
# interval
elseif ($_atom -imatch '^(?<start>\d+)\/(?<interval>\d+)$' -or $_atom -imatch '^(?<start>\d+)\-(?<end>\d+)\/(?<interval>\d+)$') {
$start = $Matches['start']
$end = $Matches['end']
$interval = [int]$Matches['interval']
if ($interval -ieq 0) {
$interval = 1
}
if ([string]::IsNullOrWhiteSpace($end)) {
$end = $_constraint[1]
}
$start = [int]$start
$end = [int]$end
$cron[$_field] += @($start)
$next = $start + $interval
while ($next -le $end) {
$cron[$_field] += $next
$next += $interval
}
}
# error
else {
throw "Invalid cron atom format found: $($_atom)"
}
# ensure cron expression values are valid
if ($null -ne $cron[$_field]) {
$cron[$_field] | ForEach-Object {
if ($_ -lt $_constraint[0] -or $_ -gt $_constraint[1]) {
throw "Value '$($_)' for $($_field) is invalid, should be between $($_constraint[0]) and $($_constraint[1])"
}
}
}
}
}
# return the parsed cron expression
return $cron
}
# convert the expression
$Atoms = ConvertFrom-CronExpression -Expression $Expression
# check day of month
if ($Atoms.DayOfMonth -notcontains $DateTime.Day) {
return $false
}
# check day of week
if ($Atoms.DayOfWeek -notcontains $DateTime.DayOfWeek.value__) {
return $false
}
# check month
if ($Atoms.Month -notcontains $DateTime.Month) {
return $false
}
# check hour
if ($Atoms.Hour -notcontains $DateTime.Hour) {
return $false
}
# check minute
if ($Atoms.Minute -notcontains $DateTime.Minute) {
return $false
}
# date is valid
return $true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment