Cronos is a .NET library for parsing Cron expressions and calculating next occurrences, that targets .NET Framework and .NET Standard. It was designed with time zones in mind, and correctly handles forward/backward Daylight Saving Time transitions (as in *nix Cron). And it's blazingly fast!
Please note this library doesn't include any task/job scheduler, it only works with Cron expressions.
- Supports standard Cron format with optional seconds.
- Supports non-standard characters like
L
,W
,#
and their combinations. - Supports reversed ranges, like
23-01
(equivalent to23,00,01
) orDEC-FEB
(equivalent toDEC,JAN,FEB
). - Supports time zones, and performs all the date/time conversions for you.
- Does not skip occurrences on Standard Time (ST) to Daylight Saving Time (DST) transitions (when the clock jumps forward).
- Does not skip interval-based occurrences on DST to ST transitions (backward jump).
- Does not retry non-interval based occurrences on DST to ST transitions (backward jump).
- When both day of week and day of month specified, AND operator will be used (different than in *nix Cron).
- For day of week field, 0 and 7 stays for Sunday, 1 for Monday.
- Contains 1000+ unit tests to ensure all is working correctly.
Cronos is distributed as a NuGet package, you can install it from the official NuGet Gallery. Please use the following command to install it using the NuGet Package Manager Console window.
PM> Install-Package Cronos
We've tried to do our best to make Cronos API as simple and predictable in corner cases as possible. To calculate the next occurrence, you need to create an instance of the CronExpression
class, and call its GetNextOccurrence
method. To learn about Cron format, please see the next section.
using Cronos;
CronExpression expression = CronExpression.Parse("* * * * *");
DateTime? nextLocal = expression.GetNextOccurrence(DateTime.Now);
DateTime? nextUtc = expression.GetNextOccurrence(DateTime.UtcNow);
Both the nextLocal
and nextUtc
will contain the next occurrence, after the given time, or null
value when it is unreachable (for example, Feb 30).
All the time zone handling logic will be done behind the scenes: nextLocal
will contain an occurrence in the TimeZoneInfo.Local
time zone with DateTimeKind.Local
specified, and nextUtc
will contain an occurrence in the TimeZoneInfo.Utc
zone with DateTimeKind.Utc
specified. All Daylight Saving Time transition's corner cases are handled automatically (see below).
When invalid Cron expression is given, an instance of the CronFormatException
class is thrown.
When dealing with custom DateTime
instances, always specify its Kind
property (for example, using the DateTime.SpecifyKind
method). When a date/time with DateTimeKind.Unspecified
is given, Cronos will throw the ArgumentException
, because it's unclear what time zone to use, and the result is prone to errors.
CronExpression expression = CronExpression.Parse("* * * * *");
DateTime from = new DateTime(2017, 03, 21, 18, 23, 00, DateTimeKind.Local); // Or DateTimeKind.Utc
DateTime? next = expression.GetNextOccurrence(from);
If you are using the DateTimeOffset
class, you either need to convert it to local or UTC first (using UtcDateTime
or LocalDateTime
properties), or specify a time zone explicitly (please see the next section).
DateTime? next = expression.GetNextOccurrence(DateTimeOffset.Now.UtcDateTime);
It is possible to specify a time zone directly, but in this case you should always pass DateTime
with DateTimeKind.Utc
flag, or use DateTimeOffset
class.
CronExpression expression = CronExpression.Parse("* * * * *");
TimeZoneInfo easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime? next = expression.GetNextOccurrence(DateTime.UtcNow, easternTimeZone));
DateTimeOffset? next = expression.GetNextOccurrence(DateTimeOffset.UtcNow, easternTimeZone);
Resulting time will be in UTC. All Daylight Saving Time transition's corner cases are handled automatically (see below).
If you want to specify seconds, use another overload of the Parse
method and specify the CronFields
argument as below:
CronExpression expression = CronExpression.Parse("*/30 * * * * *", CronFields.IncludeSeconds);
DateTime? next = expression.GetNextOccurrence(DateTime.UtcNow));
Cronos supports expressions made of second (optional), minute, hour, day of month, month, day of week fields:
Allowed values Allowed special characters Comment
┌───────────── second (optional) 0-59 * , - /
│ ┌───────────── minute 0-59 * , - /
│ │ ┌───────────── hour 0-23 * , - /
│ │ │ ┌───────────── day of month 1-31 * , - / L W ?
│ │ │ │ ┌───────────── month 1-12 or JAN-DEC * , - /
│ │ │ │ │ ┌───────────── day of week 0-7 or MON-SUN * , - / # L ? 0 and 7 means SUN
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *
Star *
*
means any value. Used to select all values within a field. For example, *
in the hour field means "every hour":
Expression | Description |
---|---|
* * * * * * |
Every second |
* * * * * |
Every minute |
30 3 * * * |
At 3:30 AM every day |
0 0 1 * * |
At midnight, on day 1 of every month |
Comma ,
Commas are used to separate items of a list.
Expression | Description |
---|---|
15,45 * * * * * |
Every minute at 15 and 45 seconds |
* * * * SAT,SUN |
Every minute on saturdays and sundays |
* * * * 6,7 |
Every minute on saturdays and sundays |
* * * * 0,6 |
Every minute on saturdays and sundays |
Hyphens -
Hyphens define ranges.
Expression | Description |
---|---|
0-30 1 * * * |
Every minute between 01:00 AM and 01:30 AM |
45-15 1 * * * |
Every minute from 1:00 AM to 01:15 AM and from 1:45 AM to 1:59 AM |
0 0 * * MON-FRI |
At 00:00, Monday through Friday |
L character
L
stands for "last". When used in the day-of-week field, it allows you to specify constructs such as "the last Friday" (5L
) of a given month. In the day-of-month field, it specifies the last day of the month.
Expression | Description |
---|---|
0 0 L * * |
At 00:00 AM on the last day of the month |
0 0 L-1 * * |
At 00:00 AM the day before the last day of the month |
0 0 * * 1L |
At 00:00 AM on the last monday of the month |
W character
You can specify the nearest weekday using W
in the day-of-month field. There are two base rules to determine occurrence: we should shift to the nearest weekday and can't shift to different month. Thus if given day is Saturday we shift to Friday, if it is Sunday we shift to Monday. But if we have 0 0 1W * *
and the 1th is Saturday we shift to the 3th Monday. And if we have 0 0 LW * *
and last day of month is Sunday we shift to that Friday.
Expression | Description |
---|---|
0 0 1W * * |
At 00:00 AM, on the first weekday of every month |
0 0 10W * * |
At 00:00 AM on the weekday nearest day 10 of every month |
0 0 LW * * |
At 00:00, on the last weekday of the month |
Hash #
Sometimes you need to specify first Friday or second Saturday. It's possible using #
character:
Expression | Description |
---|---|
0 0 * * 6#3 |
At 00:00 AM on the third Saturday of the month |
0 0 * * 1#1 |
At 00:00 AM on the first Monday of the month |
0 0 * 1 MON#1 |
At 00:00 AM on the first Monday of the January |
Question mark ?
?
is "no specific value" and a synonym of *
. It's supported but non-obligatory. 0 0 5 * *
is the same as 0 0 5 * ?
. You can specify ?
only in one field. For example, * * ? * ?
is wrong expression.
Expression | Description |
---|---|
* * * * * ? |
Every second |
* * * ? * * |
Every second |
* * * * ? |
Every minute |
* * ? * * |
Every minute |
0 0 1 * ? |
At midnight, on day 1 of every month |
0 0 ? * 1 |
At midnight every Monday |
Slash /
Slashes can be combined with ranges to specify step values.
Expression | Description |
---|---|
*/5 * * * * * |
Every 5 seconds |
0 1/5 * * * |
Every 5 hours, starting at 01:00 |
*/30 */6 * * * |
Every 30 minutes, every 6 hours: at 00:00, 00:30, 06:00, 06:30, 12:00, 12:30, 18:00, 18:30 |
0 0 15/2 * * |
At 00:00, every 2 days, starting on day 15 of the month |
0 0 * 2/3 * |
At 00:00, every 3 months, February through December |
0 0 * * 1/2 |
At 00:00, every 2 days of the week, starting on Monday |
Specify Day of month and Day of week
You can specify both Day of month and Day of week, it allows you to specify constructs such as "Friday the thirteenth".
Expression | Description |
---|---|
0 0 13 * 5 |
At 00:00, Friday the thirteenth |
0 0 13 2 5 |
At 00:00, Friday the thirteenth, only in February |
Cronos correctly handles the transition from standard time (ST) to Daylight saving time (DST).
If next occurrence falls on invalid time when the clocks jump forward then next occurrence will shift to next valid time. See example:
var expression = CronExpression.Parse("0 30 2 * * *");
var easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
// 2016-03-13 - the day when DST starts in Eastern time zone. The clocks jump from 1:59 am ST to 3:00 am DST.
// So duration from 2:00 am to 2:59 am is invalid.
var startTime = new DateTimeOffset(2016, 03, 13, 01, 50, 00, easternTimeZone.BaseUtcOffset);
// Should be scheduled to 2:30 am ST but that time is invalid. Next valid time is 3:00 am DST.
var next = expression.GetOccurrenceAfter(startTime, easternTimeZone);
Console.WriteLine("Next occurrence at " + next);
// Next occurrence at 2016-03-13 03:00:00 AM -04:00
When DST ends you set the clocks backward so you have duration which repeats twice. If you are in USA the duration was e.g. 2016/11/06 from 1:00 am to 1:59 am. If next occurrence falls on this duration behavior depends on kind of cron expression: non-interval or interval.
Cron expression is non-interval if it describes certain time of a day, e.g. "0 30 1 * * ?"
- 1:30 am every day, or "0 0,45 1,2 * * ?"
- 1:00 am, 1:45 am, 2:00 am, 2:45 am every day. In this case each cron job will be scheduled only before clock shifts. Reason is when you describe certain time of day you mean that it should be scheduled once a day regardless whether there is clock shifts in that day.
var expression = CronExpression.Parse("0 30 1 * * ?");
var easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var startTime = new DateTime(2016, 11, 06, 00, 59, 00);
var startTimeWithOffset = new DateTimeOffset(startTime, easternTimeZone.GetUtcOffset(startTime));
var next = expression.GetOccurrenceAfter(startTimeWithOffset, easternTimeZone);
Console.WriteLine("Next occurrence at " + next);
next = expression.GetOccurrenceAfter(next.Value);
Console.WriteLine("Next occurrence at " + next);
// Next occurrence at 2016-03-13 01:30:00 AM -04:00
// Next occurrence at 2016-03-13 02:30:00 AM -05:00
Cron expression is interval if it describes secondly, minutely or hourly job, e.g. "0 30 * * * ?"
, "0 * 1 * * ?"
, "0,5 */10 * * * ?"
. In this case each cron job will be scheduled before and after clock shifts.
var expression = CronExpression.Parse("0 30 * * * ?");
var easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var startTime = new DateTime(2016, 11, 06, 00, 59, 00);
var startTimeWithOffset = new DateTimeOffset(startTime, easternTimeZone.GetUtcOffset(startTime));
var next = expression.GetOccurrenceAfter(startTimeWithOffset, easternTimeZone);
Console.WriteLine("Next occurrence at " + next);
next = expression.GetOccurrenceAfter(next.Value);
Console.WriteLine("Next occurrence at " + next);
next = expression.GetOccurrenceAfter(next.Value);
Console.WriteLine("Next occurrence at " + next);
// Next occurrence at 2016-11-06 01:30:00 AM -04:00
// Next occurrence at 2016-11-06 01:30:00 AM -05:00
// Next occurrence at 2016-11-06 02:30:00 AM -05:00
Cronos is under the Apache License 2.0.