commit 216ebeaff537dd9e9d6a74d50c055c08b1dc554c Author: Simon Diesenreiter Date: Tue Apr 15 13:14:56 2025 +0200 initial commit diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj new file mode 100644 index 0000000..f63c4a9 --- /dev/null +++ b/Demo/Demo.csproj @@ -0,0 +1,12 @@ + + + + Exe + net48;net6.0 + + + + + + + diff --git a/Demo/Program.cs b/Demo/Program.cs new file mode 100644 index 0000000..4d7ff81 --- /dev/null +++ b/Demo/Program.cs @@ -0,0 +1,22 @@ +using System; + +namespace Demo +{ + class Program + { + static void Main() + { + var expression = "0-30/5 * * * * *"; + Console.WriteLine(expression); + var timer = new CronTimer(expression, "Asia/Hong_Kong", includingSeconds: true); + timer.OnOccurence += (s, ea) => Console.WriteLine($"{ea.At:T} - {DateTime.Now}"); + timer.Start(); + + while (Console.ReadKey().Key != ConsoleKey.Escape) + { + } + + timer.Stop(); + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7895df5 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# CronTimer + +Simple .net Timer that is based on cron expressions with second accuracy to fire timer events to a very specific schedule. + +Regular timers are very useful for tasks that do not really require any precision like polling a service at a rough interval but sometimes there is a need for more precision based on time. There is already a great time schedule expression syntax that originated from [Cron](https://en.wikipedia.org/wiki/Cron). Normally you would likely schedule such jobs via the operating system if they are things like on every Friday at 18:00 run a report but there are schedules that make more sense to have running in process like to not have a certain overhead of launching a whole job process or just because the process is already running. This small library makes it super easy to define such timers on a specific schedule. + +## Example + + +Fire a timer event every 10 minutes from Monday through Friday between 8:00 and 17:00 + +```c# +var timer = new CronTimer("*/10 08-17 * * 1-5", "Europe/Amsterdam", includingSeconds: false); +timer.OnOccurence += (s, ea) => Console.Out.WriteLineAsync(ea + " - " + DateTime.Now); +timer.Start(); +``` diff --git a/src/CronEventArgs.cs b/src/CronEventArgs.cs new file mode 100644 index 0000000..df87ec1 --- /dev/null +++ b/src/CronEventArgs.cs @@ -0,0 +1,6 @@ +using System; + +public class CronTimerEventArgs : EventArgs +{ + public DateTime At { get; set; } +} diff --git a/src/CronTimer.cs b/src/CronTimer.cs new file mode 100644 index 0000000..00357a8 --- /dev/null +++ b/src/CronTimer.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading; +using NCrontab; + +public class CronTimer +{ + public const string UTC = "Etc/UTC"; + + static readonly TimeSpan InfiniteTimeSpan = TimeSpan.FromMilliseconds(Timeout.Infinite); // net 3.5 + + readonly CrontabSchedule schedule; + readonly TimeZoneInfo tzi; + readonly string id; + readonly Timer t; + + public string tz { get; } + public string Expression { get; } + public event EventHandler OnOccurence; + + public DateTime Next { get; private set; } + + public CronTimer(string expression, string tz = UTC, bool includingSeconds = false) + { + Expression = expression; + this.tz = tz; + id = TimeZoneConverter.TZConvert.IanaToWindows(tz); + tzi = TimeZoneInfo.FindSystemTimeZoneById(id); + schedule = CrontabSchedule.Parse(expression, new CrontabSchedule.ParseOptions { IncludingSeconds = includingSeconds }); + Next = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi); + OnOccurence += OnOccurenceScheduleNext; + t = new Timer(s => + { + var ea = new CronTimerEventArgs + { + At = Next + }; + OnOccurence(this, ea); + }, null, InfiniteTimeSpan, InfiniteTimeSpan); + } + + void OnOccurenceScheduleNext(object sender, EventArgs e) + { + var delay = CalculateDelay(); + //Console.WriteLine($"Next for [{tz} {expression}] in {delay}."); + t.Change(delay, InfiniteTimeSpan); + } + + public void Start() + { + var delay = CalculateDelay(); + //Console.WriteLine($"Next for [{tz} {expression}] in {delay}."); + t.Change(delay, InfiniteTimeSpan); + } + + TimeSpan CalculateDelay() + { + var nowUtc = DateTime.UtcNow; + Next = schedule.GetNextOccurrence(Next); + TimeSpan delay; + if (tz != UTC) + { + var nextUtc = TimeZoneInfo.ConvertTimeToUtc(Next, tzi); + delay = nextUtc - nowUtc; + } + else + { + delay = Next - nowUtc; + } + //Console.WriteLine($"Now: {nowUtc} [utc] {now} [{tz}], Next: {next} [{tz}] {nextUtc} [utc], Delay: {delay}"); + if (delay < TimeSpan.Zero) delay = TimeSpan.Zero; + return delay; + } + + public void Stop() + { + t.Change(InfiniteTimeSpan, InfiniteTimeSpan); + } +} diff --git a/src/CronTimer.csproj b/src/CronTimer.csproj new file mode 100644 index 0000000..fcadff9 --- /dev/null +++ b/src/CronTimer.csproj @@ -0,0 +1,49 @@ + + + + latest + net461;netstandard2.1 + AnyCPU + + + + + 2.0.0 + + $(FileVersion) + + 2.0.0.0 + + $(Version) + CronTimer + https://github.com/ramonsmits + ramonsmits + Simple .net Timer that is based on cron expressions with second accuracy to fire timer events to a very specific schedule. + false + + https://github.com/ramonsmits/CronTimer/tree/$(PackageVersion) + MIT + True + snupkg + True + https://github.com/ramonsmits/CronTimer + Copyright 2022 (c) Ramon Smits + cron timer + true + true + true + true + + + + + + + + + + diff --git a/src/CronTimer.sln b/src/CronTimer.sln new file mode 100644 index 0000000..adc7e3f --- /dev/null +++ b/src/CronTimer.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30717.126 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CronTimer", "CronTimer.csproj", "{FB64C227-8615-4AE1-94E3-F9F9DF192B72}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CCDD0B34-653C-430C-9B17-5129618F8D7D}" + ProjectSection(SolutionItems) = preProject + ..\README.md = ..\README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "..\Demo\Demo.csproj", "{C2638357-1621-4422-8701-B55BFB37ACCF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.Build.0 = Release|Any CPU + {C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A639B164-6581-40DF-ADC4-479DAB467CFE} + EndGlobalSection +EndGlobal