TimeCrontab 3.11.0

dotnet add package TimeCrontab --version 3.11.0
                    
NuGet\Install-Package TimeCrontab -Version 3.11.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TimeCrontab" Version="3.11.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TimeCrontab" Version="3.11.0" />
                    
Directory.Packages.props
<PackageReference Include="TimeCrontab" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TimeCrontab --version 3.11.0
                    
#r "nuget: TimeCrontab, 3.11.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TimeCrontab@3.11.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TimeCrontab&version=3.11.0
                    
Install as a Cake Addin
#tool nuget:?package=TimeCrontab&version=3.11.0
                    
Install as a Cake Tool

TimeCrontab

license nuget dotNET China

A comprehensive .NET Cron expression parsing library that supports all Cron features.

TimeCrontab.drawio

Features

  • Supports all Cron features
  • High performance
  • Easy to extend
  • Very small, only 4KB
  • No third-party dependencies
  • Cross-platform
  • High-quality code and good unit tests
  • Supports .NET Framework 3.5+ and later versions

Installation

Install-Package TimeCrontab
dotnet add package TimeCrontab

Quick Start

We have many examples on the home page. Here is the first one to get you started.

Parsing Cron Expressions

TimeCrontab supports four Cron formats, specified via CronStringFormat:

// Default format: Minute Hour Day Month DayOfWeek
var crontab = Crontab.Parse("* * * * *");

// With year: Minute Hour Day Month DayOfWeek Year
var crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithYears);

// With seconds: Second Minute Hour Day Month DayOfWeek
var crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds);

// With seconds and year: Second Minute Hour Day Month DayOfWeek Year
var crontab = Crontab.Parse("* * * * * * *", CronStringFormat.WithSecondsAndYears);

Getting Occurrence Times

After parsing, you can get the next or previous occurrence time with the following methods:

Single Occurrence
var next = crontab.GetNextOccurrence(DateTime.Now);           // Next occurrence time
var previous = crontab.GetPreviousOccurrence(DateTime.Now);   // Previous occurrence time
All Occurrences within a Time Range
// All occurrences in the next 30 minutes
var nextOccurrences = crontab.GetNextOccurrences(DateTime.Now, DateTime.Now.AddMinutes(30));

// All occurrences in the past 30 minutes
var previousOccurrences = crontab.GetPreviousOccurrences(DateTime.Now, DateTime.Now.AddMinutes(-30));
Specified Number of Occurrences
// Next 10 occurrences
var next10 = crontab.GetNextOccurrences(DateTime.Now, 10);

// Previous 10 occurrences
var previous10 = crontab.GetPreviousOccurrences(DateTime.Now, 10);

Implementing Simple Scheduled Tasks

You can easily implement scheduled tasks using the obtained occurrence times.

Blocking Approach
var crontab = Crontab.Parse("* * * * *", CronStringFormat.Default);
while(true)
{
    Thread.Sleep(crontab.GetSleepTimeSpan(DateTime.Now));
    Console.WriteLine(DateTime.Now.ToString("G"));
}
Non-blocking Approach
var crontab = Crontab.Parse("* * * * *", CronStringFormat.Default);
Task.Factory.StartNew(async () =>
{
    while (true)
    {
        await Task.Delay(crontab.GetSleepTimeSpan(DateTime.Now));
        Console.WriteLine(DateTime.Now.ToString("G"));
    }
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
Using BackgroundService
using TimeCrontab;

namespace WorkerService;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly Crontab _crontab;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
        // Example: every minute (adjust the expression as needed)
        _crontab = Crontab.Parse("* * * * *", CronStringFormat.Default);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Calculate the time to sleep until the next occurrence
            var sleepTimeSpan = _crontab.GetSleepTimeSpan(DateTime.Now);
            await Task.Delay(sleepTimeSpan, stoppingToken);

            // Execute your business logic here
            _logger.LogInformation("Worker running at: {time}", DateTime.Now);
        }
    }
}

Macro Identifiers

TimeCrontab provides built-in macros for quickly creating common Cron expressions.

// Parse via macro strings
var secondly = Crontab.Parse("@secondly");    // Every second
var minutely = Crontab.Parse("@minutely");    // Every minute
var hourly = Crontab.Parse("@hourly");    // Every hour
var daily = Crontab.Parse("@daily");  // Every day at 00:00:00
var monthly = Crontab.Parse("@monthly");  // Every 1st day of month at 00:00:00
var weekly = Crontab.Parse("@weekly");    // Every Sunday at 00:00:00
var yearly = Crontab.Parse("@yearly");    // Every 1st day of year at 00:00:00
var weekdays = Crontab.Parse("@weekdays");    // Every Monday to Friday at 00:00:00
var weekends = Crontab.Parse("@weekends");    // Every Saturday and Sunday at 00:00:00

// Static properties
var secondly = Crontab.Secondly;    // Every second
var minutely = Crontab.Minutely;    // Every minute
var hourly = Crontab.Hourly;    // Every hour
var daily = Crontab.Daily;  // Every day at 00:00:00
var monthly = Crontab.Monthly;  // Every 1st day of month at 00:00:00
var weekly = Crontab.Weekly;    // Every Sunday at 00:00:00
var yearly = Crontab.Yearly;    // Every 1st day of year at 00:00:00
var weekdays = Crontab.Weekdays;    // Every Monday to Friday at 00:00:00
var weekends = Crontab.Weekends;    // Every Saturday and Sunday at 00:00:00

Macro At Identifiers

Allows specifying exact second, minute, hour etc. on top of macros.

// Every 3rd second
var crontab = Crontab.SecondlyAt(3);
// Every 3,5,6 seconds
var crontab = Crontab.SecondlyAt(3, 5, 6);

// Every minute at the 3rd second
var crontab = Crontab.MinutelyAt(3);
// Every minute at the 3rd,5th,6th second
var crontab = Crontab.MinutelyAt(3, 5, 6);

// Every hour at the 3rd minute
var crontab = Crontab.HourlyAt(3);
// Every hour at the 3rd,5th,6th minute
var crontab = Crontab.HourlyAt(3, 5, 6);

// Every day at the 3rd hour
var crontab = Crontab.DailyAt(3);
// Every day at the 3rd,5th,6th hour
var crontab = Crontab.DailyAt(3, 5, 6);

// Every month on the 3rd day at midnight
var crontab = Crontab.MonthlyAt(3);
// Every month on the 3rd,5th,6th day at midnight
var crontab = Crontab.MonthlyAt(3, 5, 6);

// Every week on Wednesday at midnight
var crontab = Crontab.WeeklyAt(3);
var crontab = Crontab.WeeklyAt("WED");  // SUN, MON, TUE, WED, THU, FRI, SAT
// Every week on Wednesday, Friday, Saturday at midnight
var crontab = Crontab.WeeklyAt(3, 5, 6);
var crontab = Crontab.WeeklyAt("WED", "FRI", "SAT");
// Mixed
var crontab = Crontab.WeeklyAt(3, "FRI", 6);

// Every year in March at midnight
var crontab = Crontab.YearlyAt(3);
var crontab = Crontab.YearlyAt("MAR");  // JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
// Every year in March, May, June at midnight
var crontab = Crontab.YearlyAt(3);
var crontab = Crontab.YearlyAt(3, 5, 6);
var crontab = Crontab.YearlyAt("MAR", "MAY", "JUN");
// Mixed
var crontab = Crontab.YearlyAt(3, "MAY", 6);

Supporting R Random Moment

R is a special CRON expression character that allows you to specify a randomly generated moment. You can also use H instead of R, they are completely equivalent. For example, R 0 0 * * ? * means triggering at a random second (0‑59) every day at 00:00. R R R 15W * ? * means triggering at a random moment (seconds, minutes, hours) on the 15th day of each month. If the 15th is a Saturday, it fires on Friday (the 14th); if it is a Sunday, it fires on Monday (the 16th). Reference

// Full-range random (seconds 0-59)
var crontab = Crontab.Parse("R 0 0 * * ? *", CronStringFormat.WithSecondsAndYears);

R also supports specifying a random interval in the format R(min-max). This is very useful for staggering a large number of scheduled tasks to avoid system pressure caused by simultaneous triggering.

// Random seconds between 30~59
var crontab = Crontab.Parse("R(30-59) * * * * *", CronStringFormat.WithSeconds);

// Random minutes between 1~5
var crontab = Crontab.Parse("* R(1-5) * * * *", CronStringFormat.WithSeconds);

// Random hours between 10~20
var crontab = Crontab.Parse("* * R(10-20) * * *", CronStringFormat.WithSeconds);

R also supports interval random with a step, in the formats R(min-max)/step or R/step. Candidates are filtered by the step within the given range, and one value is randomly selected.

// Every 5 seconds between 0~59 (0,5,10,...,55)
var crontab = Crontab.Parse("R(0-59)/5 * * * * *", CronStringFormat.WithSeconds);

// Every 10 minutes between 0~59 (0,10,20,30,40,50)
var crontab = Crontab.Parse("* R(0-59)/10 * * * *", CronStringFormat.WithSeconds);

// Every 6 hours between 0~23 (0,6,12,18)
var crontab = Crontab.Parse("* * R(0-23)/6 * * *", CronStringFormat.WithSeconds);

// Step 2 between 1~5 (1,3,5)
var crontab = Crontab.Parse("R(1-5)/2 * * * * *", CronStringFormat.WithSeconds);

// Full-range step: random every 10 seconds (0,10,20,30,40,50)
var crontab = Crontab.Parse("R/10 * * * * *", CronStringFormat.WithSeconds);

R also supports discrete value random, in the format R(value1,value2,value3), which randomly selects from the specified values.

// Random seconds among 1,5,10,12
var crontab = Crontab.Parse("R(1,5,10,12) * * * * *", CronStringFormat.WithSeconds);

// Random minutes among 0,15,30,45
var crontab = Crontab.Parse("* R(0,15,30,45) * * * *", CronStringFormat.WithSeconds);

// Random hours among 8,12,18
var crontab = Crontab.Parse("* * R(8,12,18) * * *", CronStringFormat.WithSeconds);

R can also be combined with # to indicate the Nth random day of the week in a month. For example, R#3 randomly selects a day of the week at parse time, and then fires on the 3rd occurrence of that weekday in the month. H#3 works the same way.

// Trigger at 00:00 on the 3rd random weekday of each month
var crontab = Crontab.Parse("0 0 * * R#3");

More Documentation

Documentation

You can find the TimeCrontab documentation on the home page.

Tests

public class TimeCrontabUnitTests
{
    private readonly ITestOutputHelper _testOutput;
    public TimeCrontabUnitTests(ITestOutputHelper testOutput)
    {
        _testOutput = testOutput;
    }

    [Theory]
    [InlineData("* * * * *", "* * * * *", CronStringFormat.Default)]
    [InlineData("0 0 31W * *", "0 0 31W * *", CronStringFormat.Default)]
    [InlineData("0 23 ? * MON-FRI", "0 23 ? * 1-5", CronStringFormat.Default)]
    [InlineData("*/5 * * * *", "*/5 * * * *", CronStringFormat.Default)]
    [InlineData("30 11 * * 1-5", "30 11 * * 1-5", CronStringFormat.Default)]
    [InlineData("23 12 * JAN *", "23 12 * 1 *", CronStringFormat.Default)]
    [InlineData("* * * * MON#3", "* * * * 1#3", CronStringFormat.Default)]
    [InlineData("*/5 * L JAN *", "*/5 * L 1 *", CronStringFormat.Default)]
    [InlineData("0 0 ? 1 MON#1", "0 0 ? 1 1#1", CronStringFormat.Default)]
    [InlineData("0 0 LW * *", "0 0 LW * *", CronStringFormat.Default)]
    [InlineData("0 30 10-13 ? * WED,FRI", "0 30 10-13 ? * 3,5", CronStringFormat.WithSeconds)]
    [InlineData("0 */5 * * * *", "0 */5 * * * *", CronStringFormat.WithSeconds)]
    [InlineData("0 0/1 * * * ?", "0 */1 * * * ?", CronStringFormat.WithSeconds)]
    [InlineData("5-10 30-35 10-12 * * *", "5-10 30-35 10-12 * * *", CronStringFormat.WithSeconds)]
    [InlineData("20/10 * * * * ?", "20/10 * * * * ?", CronStringFormat.WithSeconds)]
    public void TestParse(string expression, string outputString, CronStringFormat format)
    {
        var output = Crontab.Parse(expression, format).ToString();
        Assert.Equal(outputString, output);
    }

    [Theory]
    [InlineData("* * * * *", "2022-01-01 00:01:00", CronStringFormat.Default)]
    [InlineData("0 0 31W * *", "2022-01-31 00:00:00", CronStringFormat.Default)]
    [InlineData("0 23 ? * MON-FRI", "2022-01-03 23:00:00", CronStringFormat.Default)]
    [InlineData("*/5 * * * *", "2022-01-01 00:05:00", CronStringFormat.Default)]
    [InlineData("30 11 * * 1-5", "2022-01-03 11:30:00", CronStringFormat.Default)]
    [InlineData("23 12 * JAN *", "2022-01-01 12:23:00", CronStringFormat.Default)]
    [InlineData("* * * * MON#3", "2022-01-17 00:00:00", CronStringFormat.Default)]
    [InlineData("*/5 * L JAN *", "2022-01-31 00:00:00", CronStringFormat.Default)]
    [InlineData("0 0 ? 1 MON#1", "2022-01-03 00:00:00", CronStringFormat.Default)]
    [InlineData("0 0 LW * *", "2022-01-31 00:00:00", CronStringFormat.Default)]
    [InlineData("0 30 10-13 ? * WED,FRI", "2022-01-05 10:30:00", CronStringFormat.WithSeconds)]
    [InlineData("0 */5 * * * *", "2022-01-01 00:05:00", CronStringFormat.WithSeconds)]
    [InlineData("0 0/1 * * * ?", "2022-01-01 00:01:00", CronStringFormat.WithSeconds)]
    [InlineData("5-10 30-35 10-12 * * *", "2022-01-01 10:30:05", CronStringFormat.WithSeconds)]
    [InlineData("20/10 * * * * ?", "2022-01-01 00:00:20", CronStringFormat.WithSeconds)]
    [InlineData("20/30 * * * * ?", "2022-01-01 00:00:20", CronStringFormat.WithSeconds)]
    public void TestGetNextOccurence(string expression, string nextOccurenceString, CronStringFormat format)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var nextOccurence = crontab.GetNextOccurrence(beginTime);
        Assert.Equal(nextOccurenceString, nextOccurence.ToString("yyyy-MM-dd HH:mm:ss"));
    }

    [Theory]
    [InlineData("* * * * *", "2021-12-31 23:59:00", CronStringFormat.Default)]
    [InlineData("0 0 31W * *", "2021-12-31 00:00:00", CronStringFormat.Default)]
    [InlineData("0 23 ? * MON-FRI", "2021-12-31 23:00:00", CronStringFormat.Default)]
    [InlineData("*/5 * * * *", "2021-12-31 23:55:00", CronStringFormat.Default)]
    [InlineData("30 11 * * 1-5", "2021-12-31 11:30:00", CronStringFormat.Default)]
    [InlineData("23 12 * JAN *", "2021-01-31 12:23:00", CronStringFormat.Default)]
    [InlineData("* * * * MON#3", "2021-12-20 23:59:00", CronStringFormat.Default)]
    [InlineData("*/5 * L JAN *", "2021-01-31 23:55:00", CronStringFormat.Default)]
    [InlineData("0 0 ? 1 MON#1", "2021-01-04 00:00:00", CronStringFormat.Default)]
    [InlineData("0 0 LW * *", "2021-12-31 00:00:00", CronStringFormat.Default)]
    [InlineData("0 30 10-13 ? * WED,FRI", "2021-12-31 13:30:00", CronStringFormat.WithSeconds)]
    [InlineData("0 */5 * * * *", "2021-12-31 23:55:00", CronStringFormat.WithSeconds)]
    [InlineData("0 0/1 * * * ?", "2021-12-31 23:59:00", CronStringFormat.WithSeconds)]
    [InlineData("5-10 30-35 10-12 * * *", "2021-12-31 12:35:10", CronStringFormat.WithSeconds)]
    [InlineData("20/10 * * * * ?", "2021-12-31 23:59:50", CronStringFormat.WithSeconds)]
    [InlineData("20/30 * * * * ?", "2021-12-31 23:59:50", CronStringFormat.WithSeconds)]
    public void GetPreviousOccurrence(string expression, string previousOccurenceString, CronStringFormat format)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var previous = crontab.GetPreviousOccurrence(beginTime);
        Assert.Equal(previousOccurenceString, previous.ToString("yyyy-MM-dd HH:mm:ss"));
    }

    [Theory]
    [InlineData("R 0 0 * * ? *", "R 0 0 * * ? *", CronStringFormat.WithSecondsAndYears)]
    [InlineData("R R R 15W * ? *", "R R R 15W * ? *", CronStringFormat.WithSecondsAndYears)]
    [InlineData("R * * * * *", "R * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* R * * * *", "* R * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R * * *", "* * R * * *", CronStringFormat.WithSeconds)]
    public void TestParse_Random(string expression, string outputString, CronStringFormat format)
    {
        var output = Crontab.Parse(expression, format).ToString();
        Assert.Equal(outputString, output);
    }

    [Theory]
    [InlineData("R(30-59) * * * * *", "R(30-59) * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* R(1-5) * * * *", "* R(1-5) * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R(5-10) * * *", "* * R(5-10) * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(0-59) * * * * *", "R * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(10-10) * * * * *", "R(10-10) * * * * *", CronStringFormat.WithSeconds)]
    public void TestParse_RandomRange(string expression, string outputString, CronStringFormat format)
    {
        var output = Crontab.Parse(expression, format).ToString();
        Assert.Equal(outputString, output);
    }

    [Theory]
    [InlineData("R(1,5,10,12) * * * * *", "R(1,5,10,12) * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* R(0,15,30,45) * * * *", "* R(0,15,30,45) * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R(8,12,18) * * *", "* * R(8,12,18) * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(5,10,15) * * * * *", "R(5,10,15) * * * * *", CronStringFormat.WithSeconds)]
    public void TestParse_RandomDiscrete(string expression, string outputString, CronStringFormat format)
    {
        var output = Crontab.Parse(expression, format).ToString();
        Assert.Equal(outputString, output);
    }

    [Theory]
    [InlineData("R 0 0 * * ? *", CronStringFormat.WithSecondsAndYears, 0, 59)]
    [InlineData("* R 0 * * ? *", CronStringFormat.WithSecondsAndYears, 0, 59)]
    [InlineData("* * R * * ? *", CronStringFormat.WithSecondsAndYears, 0, 23)]
    public void TestNextOccurrence_RandomValueInRange(string expression, CronStringFormat format, int min, int max)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var next = crontab.GetNextOccurrence(beginTime);

        int actualValue = GetRandomFieldValue(next, expression);
        Assert.InRange(actualValue, min, max);
        _testOutput.WriteLine($"Random value: {actualValue}");
    }

    [Theory]
    [InlineData("R(30-59) * * * * *", CronStringFormat.WithSeconds, 30, 59)]
    [InlineData("* R(10-20) * * * *", CronStringFormat.WithSeconds, 10, 20)]
    [InlineData("* * R(5-10) * * *", CronStringFormat.WithSeconds, 5, 10)]
    [InlineData("R(10-10) * * * * *", CronStringFormat.WithSeconds, 10, 10)]
    public void TestNextOccurrence_RandomRange(string expression, CronStringFormat format, int min, int max)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var next = crontab.GetNextOccurrence(beginTime);

        int actualValue = GetRandomFieldValue(next, expression);
        Assert.InRange(actualValue, min, max);
        _testOutput.WriteLine($"Random range value: {actualValue}");
    }

    [Theory]
    [InlineData("R(1,5,10,12) * * * * *", CronStringFormat.WithSeconds, new int[] { 1, 5, 10, 12 })]
    [InlineData("* R(0,15,30,45) * * * *", CronStringFormat.WithSeconds, new int[] { 0, 15, 30, 45 })]
    [InlineData("* * R(8,12,18) * * *", CronStringFormat.WithSeconds, new int[] { 8, 12, 18 })]
    [InlineData("R(10,10,10) * * * * *", CronStringFormat.WithSeconds, new int[] { 10 })]
    public void TestNextOccurrence_RandomDiscrete(string expression, CronStringFormat format, int[] validValues)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var next = crontab.GetNextOccurrence(beginTime);

        int actualValue = GetRandomFieldValue(next, expression);
        Assert.Contains(actualValue, validValues);
        _testOutput.WriteLine($"Random discrete value: {actualValue}");
    }

    [Fact]
    public void TestMultiRandomFieldNextOccurrence()
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse("R R R 15W * ? *", CronStringFormat.WithSecondsAndYears);
        var next = crontab.GetNextOccurrence(beginTime);

        Assert.InRange(next.Second, 0, 59);
        Assert.InRange(next.Minute, 0, 59);
        Assert.InRange(next.Hour, 0, 23);

        Assert.Equal(14, next.Day);
        Assert.Equal(1, next.Month);
        Assert.Equal(2022, next.Year);
    }

    [Theory]
    [InlineData("R,30 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* R,5 * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R,10 * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(30-59),20 * * * * *", CronStringFormat.WithSeconds)]
    public void TestRandomCombinedWithOtherValuesThrows(string expression, CronStringFormat format)
    {
        Assert.Throws<TimeCrontabException>(() => Crontab.Parse(expression, format));
    }

    [Theory]
    [InlineData("R(60-30) * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* R(-1-5) * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R(0-60) * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(abc-def) * * * * *", CronStringFormat.WithSeconds)]
    public void TestInvalidRandomRangeThrows(string expression, CronStringFormat format)
    {
        Assert.Throws<TimeCrontabException>(() => Crontab.Parse(expression, format));
    }

    [Theory]
    [InlineData("R(61) * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(1,abc,10) * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R(25) * * *", CronStringFormat.WithSeconds)]
    [InlineData("R() * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(1,,5) * * * * *", CronStringFormat.WithSeconds)]
    public void TestInvalidRandomDiscreteThrows(string expression, CronStringFormat format)
    {
        Assert.Throws<TimeCrontabException>(() => Crontab.Parse(expression, format));
    }

    [Theory]
    [InlineData("R(1,5,10,12,30) * * * * *", CronStringFormat.WithSeconds)]
    public void TestRandomDiscreteCombinedWithOtherValuesNotThrow(string expression, CronStringFormat format)
    {
        var crontab = Crontab.Parse(expression, format);
        Assert.NotNull(crontab);
    }

    [Theory]
    [InlineData("* R(0,60) * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R(0,24) * * *", CronStringFormat.WithSeconds)]
    public void TestDiscreteValueOutOfRangeThrows(string expression, CronStringFormat format)
    {
        Assert.Throws<TimeCrontabException>(() => Crontab.Parse(expression, format));
    }

    [Theory]
    [InlineData("R(0-59)/5 * * * * *", "R(0-59)/5 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* R(0-59)/10 * * * *", "* R(0-59)/10 * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R(0-23)/2 * * *", "* * R(0-23)/2 * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(1-5)/2 * * * * *", "R(1-5)/2 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(1-5)/1 * * * * *", "R(1-5)/1 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R/5 * * * * *", "R/5 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* R/10 * * * *", "* R/10 * * * *", CronStringFormat.WithSeconds)]
    [InlineData("* * R/2 * * *", "* * R/2 * * *", CronStringFormat.WithSeconds)]
    public void TestParse_RandomStep(string expression, string outputString, CronStringFormat format)
    {
        var output = Crontab.Parse(expression, format).ToString();
        Assert.Equal(outputString, output);
    }

    [Theory]
    [InlineData("R(0-59)/10 * * * * *", CronStringFormat.WithSeconds, new int[] { 0, 10, 20, 30, 40, 50 })]
    [InlineData("* R(0-59)/15 * * * *", CronStringFormat.WithSeconds, new int[] { 0, 15, 30, 45 })]
    [InlineData("* * R(0-23)/6 * * *", CronStringFormat.WithSeconds, new int[] { 0, 6, 12, 18 })]
    [InlineData("R(1-5)/2 * * * * *", CronStringFormat.WithSeconds, new int[] { 1, 3, 5 })]
    [InlineData("R/10 * * * * *", CronStringFormat.WithSeconds, new int[] { 0, 10, 20, 30, 40, 50 })]
    [InlineData("* R/15 * * * *", CronStringFormat.WithSeconds, new int[] { 0, 15, 30, 45 })]
    [InlineData("* * R/6 * * *", CronStringFormat.WithSeconds, new int[] { 0, 6, 12, 18 })]
    public void TestNextOccurrence_RandomStep(string expression, CronStringFormat format, int[] validValues)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var next = crontab.GetNextOccurrence(beginTime);

        int actualValue = GetRandomFieldValue(next, expression);
        Assert.Contains(actualValue, validValues);
        _testOutput.WriteLine($"Random step value: {actualValue}");
    }

    [Theory]
    [InlineData("R(0-59)/0 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(0-59)/-5 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(0-59)/abc * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R(5-1)/2 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R/0 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R/-5 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("R/abc * * * * *", CronStringFormat.WithSeconds)]
    public void TestInvalidRandomStepThrows(string expression, CronStringFormat format)
    {
        Assert.Throws<TimeCrontabException>(() => Crontab.Parse(expression, format));
    }

    [Theory]
    [InlineData("* * * * *", CronStringFormat.Default, 5)]
    [InlineData("*/5 * * * *", CronStringFormat.Default, 10)]
    [InlineData("0 0/1 * * * ?", CronStringFormat.WithSeconds, 3)]
    public void TestGetNextOccurrencesCount(string expression, CronStringFormat format, int count)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var occurrences = crontab.GetNextOccurrences(beginTime, count).ToList();

        Assert.Equal(count, occurrences.Count);

        for (int i = 0; i < occurrences.Count - 1; i++)
        {
            Assert.True(occurrences[i] < occurrences[i + 1]);
        }

        Assert.All(occurrences, dt => Assert.True(dt > beginTime));
    }

    [Theory]
    [InlineData("* * * * *", CronStringFormat.Default, 5)]
    [InlineData("*/5 * * * *", CronStringFormat.Default, 10)]
    [InlineData("0 0/1 * * * ?", CronStringFormat.WithSeconds, 3)]
    public void TestGetPreviousOccurrencesCount(string expression, CronStringFormat format, int count)
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse(expression, format);
        var occurrences = crontab.GetPreviousOccurrences(beginTime, count).ToList();

        Assert.Equal(count, occurrences.Count);

        for (int i = 0; i < occurrences.Count - 1; i++)
        {
            Assert.True(occurrences[i] > occurrences[i + 1]);
        }

        Assert.All(occurrences, dt => Assert.True(dt < beginTime));
    }

    [Fact]
    public void TestGetNextOccurrencesCountWithRandomExpression()
    {
        var beginTime = new DateTime(2022, 1, 1, 0, 0, 0);
        var crontab = Crontab.Parse("R(30-59) * * * * *", CronStringFormat.WithSeconds);
        var occurrences = crontab.GetNextOccurrences(beginTime, 5).ToList();

        Assert.Equal(5, occurrences.Count);
        Assert.All(occurrences, dt => Assert.InRange(dt.Second, 30, 59));

        for (int i = 0; i < 4; i++)
        {
            Assert.True(occurrences[i] < occurrences[i + 1]);
        }
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public void TestGetNextOccurrencesCountInvalidCount(int invalidCount)
    {
        var crontab = Crontab.Parse("* * * * *");
        Assert.Throws<ArgumentOutOfRangeException>(() => crontab.GetNextOccurrences(DateTime.Now, invalidCount).ToList());
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public void TestGetPreviousOccurrencesCountInvalidCount(int invalidCount)
    {
        var crontab = Crontab.Parse("* * * * *");
        Assert.Throws<ArgumentOutOfRangeException>(() => crontab.GetPreviousOccurrences(DateTime.Now, invalidCount).ToList());
    }

    [Theory]
    [InlineData("* * * * R#3", CronStringFormat.Default)]
    [InlineData("* * * * H#5", CronStringFormat.Default)]
    [InlineData("0 0 * * R#1", CronStringFormat.Default)]
    [InlineData("0 0 * * H#2", CronStringFormat.Default)]
    public void TestParse_RandomHash_Success(string expression, CronStringFormat format)
    {
        var crontab = Crontab.Parse(expression, format);
        Assert.NotNull(crontab);
    }

    [Theory]
    [InlineData("* * R#3 * * *", CronStringFormat.WithSeconds)]
    [InlineData("R#3 * * * * *", CronStringFormat.WithSeconds)]
    [InlineData("0 0 * * * H#2", CronStringFormat.WithYears)]
    public void TestParse_RandomHash_InvalidField_Throws(string expression, CronStringFormat format)
    {
        Assert.Throws<TimeCrontabException>(() => Crontab.Parse(expression, format));
    }

    [Theory]
    [InlineData("* * * * R#0", CronStringFormat.Default)]
    [InlineData("* * * * R#6", CronStringFormat.Default)]
    [InlineData("* * * * H#-1", CronStringFormat.Default)]
    [InlineData("* * * * H#abc", CronStringFormat.Default)]
    public void TestParse_RandomHash_InvalidNumber_Throws(string expression, CronStringFormat format)
    {
        Assert.Throws<TimeCrontabException>(() => Crontab.Parse(expression, format));
    }

    [Fact]
    public void TestRandomHash_Occurrence_IsValid()
    {
        var crontab = Crontab.Parse("* * * * R#3");
        var next = crontab.GetNextOccurrence(new DateTime(2022, 1, 1, 0, 0, 0));
        Assert.InRange((int)next.DayOfWeek, 0, 6);
    }

    private static int GetRandomFieldValue(DateTime dateTime, string expression)
    {
        var parts = expression.Split(' ');

        for (int i = 0; i < 3; i++)
        {
            if (parts[i].StartsWith("R"))
            {
                return i switch
                {
                    0 => dateTime.Second,
                    1 => dateTime.Minute,
                    2 => dateTime.Hour,
                    _ => throw new InvalidOperationException()
                };
            }
        }

        throw new ArgumentException("No random field found in expression");
    }
}

Contributing

The primary goal of this repository is to continue developing TimeCrontab, making it faster and easier to use. Development of TimeCrontab is publicly conducted on Gitee, and we appreciate community contributions for bug fixes and improvements.

License

TimeCrontab is released under the MIT open source license.

alternate text is missing from this package README image

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
.NET Core netcoreapp1.0 was computed.  netcoreapp1.1 was computed.  netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard1.0 is compatible.  netstandard1.1 was computed.  netstandard1.2 was computed.  netstandard1.3 was computed.  netstandard1.4 was computed.  netstandard1.5 was computed.  netstandard1.6 was computed.  netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net35 is compatible.  net40 was computed.  net403 was computed.  net45 was computed.  net451 was computed.  net452 is compatible.  net46 was computed.  net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen30 was computed.  tizen40 was computed.  tizen60 was computed. 
Universal Windows Platform uap was computed.  uap10.0 was computed. 
Windows Phone wp8 was computed.  wp81 was computed.  wpa81 was computed. 
Windows Store netcore was computed.  netcore45 was computed.  netcore451 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETFramework 3.5

    • No dependencies.
  • .NETFramework 4.5.2

    • No dependencies.
  • .NETStandard 1.0

  • .NETStandard 2.0

    • No dependencies.
  • .NETStandard 2.1

    • No dependencies.
  • net10.0

    • No dependencies.
  • net11.0

    • No dependencies.
  • net5.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net7.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (6)

Showing the top 5 NuGet packages that depend on TimeCrontab:

Package Downloads
Sundial

.NET 功能齐全的开源分布式作业调度系统,可从最小的应用程序到大型企业系统使用。

M56X.Core

Package Description

Galosys.Foundation.FreeScheduler

Galosys.Foundation快速开发库

Daily.CoreSwim

支持分布式的 定时调度作业平台

NHJ212DataReceiver

基于《污染物在线监控(监测)系统数据传输标准HJ 212-2017》开发上位机接收程序

GitHub repositories (3)

Showing the top 3 popular GitHub repositories that depend on TimeCrontab:

Repository Stars
ClassIsland/ClassIsland
一款功能强、可定制、跨平台,适用于班级多媒体屏幕的课表信息显示工具,可以一目了然地显示各种信息。
YSGStudyHards/DotNetExercises
⚔【DotNetGuide专栏C#/.NET/.NET Core编程技巧练习集】C#/.NET/.NET Core编程常用语法、算法、技巧、中间件、类库、工作业务实操练习集,配套详细的文章教程和代码示例,助力快速掌握C#/.NET/.NET Core中各种编程常用语法、算法、技巧、中间件、类库、工作业务实操等等。
hupo376787/WeiboAlbumDownloader
微博相册下载工具C#版,批量采集指定微博账号下的所有图片/视频/LivePhoto。
Version Downloads Last Updated
3.11.0 0 7/5/2026
3.10.0 0 7/5/2026
3.9.3 166 6/23/2026
3.9.2 159 6/23/2026 3.9.2 is deprecated because it is no longer maintained and has critical bugs.
3.9.1 122 6/23/2026 3.9.1 is deprecated because it is no longer maintained and has critical bugs.
3.9.0 169 6/18/2026 3.9.0 is deprecated because it is no longer maintained and has critical bugs.
3.7.3 938 5/4/2026 3.7.3 is deprecated because it is no longer maintained and has critical bugs.
3.7.2 270 4/27/2026 3.7.2 is deprecated because it is no longer maintained and has critical bugs.
3.7.1 1,055 1/24/2026 3.7.1 is deprecated because it is no longer maintained and has critical bugs.
3.7.0 4,247 4/22/2025 3.7.0 is deprecated because it is no longer maintained and has critical bugs.
3.6.0 4,434 2/11/2025 3.6.0 is deprecated because it is no longer maintained and has critical bugs.
3.5.1 19,893 1/7/2025 3.5.1 is deprecated because it is no longer maintained and has critical bugs.
3.5.0 675 12/19/2024 3.5.0 is deprecated because it is no longer maintained and has critical bugs.
3.4.0 6,744 3/19/2024 3.4.0 is deprecated because it is no longer maintained and has critical bugs.
3.3.6 5,271 10/13/2023 3.3.6 is deprecated because it is no longer maintained and has critical bugs.
3.3.5 4,210 6/28/2023 3.3.5 is deprecated because it is no longer maintained and has critical bugs.
3.3.4 1,166 6/14/2023 3.3.4 is deprecated because it is no longer maintained and has critical bugs.
3.3.3 5,712 3/20/2023 3.3.3 is deprecated because it is no longer maintained and has critical bugs.
Loading failed