-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.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