Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mmunchandersen/cd5038957a5c29515a3b0b91289fa02b to your computer and use it in GitHub Desktop.
Save mmunchandersen/cd5038957a5c29515a3b0b91289fa02b 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()]
$DateTime = $null
)
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++)
{
$_cronExp = @{
'Range' = $null;
'Values' = $null;
}
$_atom = $atoms[$i]
$_field = $fields[$i]
$_constraint = $constraints.MinMax[$i]
$_aliases = $aliases[$_field]
# replace day of week and months with numbers
switch ($_field)
{
{ $_field -ieq 'month' -or $_field -ieq 'dayofweek' }
{
while ($_atom -imatch $aliasRgx) {
$_alias = $_aliases[$Matches['tag']]
if ($null -eq $_alias) {
throw "Invalid $($_field) alias found: $($Matches['tag'])"
}
$_atom = $_atom -ireplace $Matches['tag'], $_alias
$_atom -imatch $aliasRgx | Out-Null
}
}
}
# 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+$') {
$_cronExp.Values = @([int]$_atom)
}
# range
elseif ($_atom -imatch '^(?<min>\d+)\-(?<max>\d+)$') {
$_cronExp.Range = @{ 'Min' = [int]($Matches['min'].Trim()); 'Max' = [int]($Matches['max'].Trim()); }
}
# array
elseif ($_atom -imatch '^[\d,]+$') {
$_cronExp.Values = [int[]](@($_atom -split ',').Trim())
}
# interval
elseif ($_atom -imatch '(?<start>(\d+|\*))\/(?<interval>\d+)$') {
$start = $Matches['start']
$interval = [int]$Matches['interval']
if ($interval -ieq 0) {
$interval = 1
}
if ([string]::IsNullOrWhiteSpace($start) -or $start -ieq '*') {
$start = 0
}
$start = [int]$start
$_cronExp.Values = @($start)
$next = $start + $interval
while ($next -le $_constraint[1]) {
$_cronExp.Values += $next
$next += $interval
}
}
# error
else {
throw "Invalid cron atom format found: $($_atom)"
}
# ensure cron expression values are valid
if ($null -ne $_cronExp.Range) {
if ($_cronExp.Range.Min -gt $_cronExp.Range.Max) {
throw "Min value for $($_field) should not be greater than the max value"
}
if ($_cronExp.Range.Min -lt $_constraint[0]) {
throw "Min value '$($_cronExp.Range.Min)' for $($_field) is invalid, should be greater than/equal to $($_constraint[0])"
}
if ($_cronExp.Range.Max -gt $_constraint[1]) {
throw "Max value '$($_cronExp.Range.Max)' for $($_field) is invalid, should be less than/equal to $($_constraint[1])"
}
}
if ($null -ne $_cronExp.Values) {
$_cronExp.Values | ForEach-Object {
if ($_ -lt $_constraint[0] -or $_ -gt $_constraint[1]) {
throw "Value '$($_)' for $($_field) is invalid, should be between $($_constraint[0]) and $($_constraint[1])"
}
}
}
# assign value
$cron[$_field] = $_cronExp
}
# post validation for month/days in month
if ($null -ne $cron['Month'].Values -and $null -ne $cron['DayOfMonth'].Values)
{
foreach ($mon in $cron['Month'].Values) {
foreach ($day in $cron['DayOfMonth'].Values) {
if ($day -gt $constraints.DaysInMonths[$mon - 1]) {
throw "$($constraints.Months[$mon - 1]) only has $($constraints.DaysInMonths[$mon - 1]) days, but $($day) was supplied"
}
}
}
}
# return the parsed cron expression
return $cron
}
function Test-RangeAndValue($AtomContraint, $NowValue) {
if ($null -ne $AtomContraint.Range) {
if ($NowValue -lt $AtomContraint.Range.Min -or $NowValue -gt $AtomContraint.Range.Max) {
return $false
}
}
elseif ($AtomContraint.Values -inotcontains $NowValue) {
return $false
}
return $true
}
# current time
if ($null -eq $DateTime) {
$DateTime = [datetime]::Now
}
# convert the expression
$Atoms = ConvertFrom-CronExpression -Expression $Expression
# check day of month
if (!(Test-RangeAndValue -AtomContraint $Atoms.DayOfMonth -NowValue $DateTime.Day)) {
return $false
}
# check day of week
if (!(Test-RangeAndValue -AtomContraint $Atoms.DayOfWeek -NowValue ([int]$DateTime.DayOfWeek))) {
return $false
}
# check month
if (!(Test-RangeAndValue -AtomContraint $Atoms.Month -NowValue $DateTime.Month)) {
return $false
}
# check hour
if (!(Test-RangeAndValue -AtomContraint $Atoms.Hour -NowValue $DateTime.Hour)) {
return $false
}
# check minute
if (!(Test-RangeAndValue -AtomContraint $Atoms.Minute -NowValue $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