diff --git a/README.md b/README.md index 3fe1bfe..3ede616 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,21 @@ static async Task Main(string[] args) ); // You can also create rules from cron expressions; * or comma separated lists are supported - // (/ and - are NOT supported). // Format: minute (0..59), hour (0..23), dayOfMonth (1..31), month (1..12), dayOfWeek (0=Sunday..6). // Seconds will always be zero. var s4 = runtime.CreateSchedule() - .FromCron("0,20,40 * * * *") - .WithName("Every20Sec") //Optional ID for your reference + .FromCron("0,1/5 * * * *") + .WithName("Every5Min") //Optional ID for your reference + .Execute(async (e, token) => { + if(!token.IsCancellationRequested) + Console.WriteLine($"Load me from config and change me without recompiling!"); + return true; + }); + + // Slash and Dashes are now supported! + var s5 = runtime.CreateSchedule() + .FromCron("0 1/2 * * 0-4") + .WithName("Every2HoursSundayThroughThursday") //Optional ID for your reference .Execute(async (e, token) => { if(!token.IsCancellationRequested) Console.WriteLine($"Load me from config and change me without recompiling!"); @@ -69,7 +78,7 @@ static async Task Main(string[] args) }); // Finally, there are helper methods ExecuteEvery*() that execute a task at a given interval. - var s4 = runtime.CreateSchedule() + var s6 = runtime.CreateSchedule() // Run the task a 0 minutes and 0 seconds past the hours 0, 6, 12, and 18 .ExecuteEveryHour(0, 6, 12, 18) .Execute(async (e, token) => { diff --git a/src/ScheduleRule.cs b/src/ScheduleRule.cs index f65ba25..f55a599 100644 --- a/src/ScheduleRule.cs +++ b/src/ScheduleRule.cs @@ -4,6 +4,8 @@ * https://github.com/pettijohn/TaskSchedulerEngine */ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; @@ -13,16 +15,12 @@ [assembly: InternalsVisibleToAttribute("TaskSchedulerEngineTests")] -namespace TaskSchedulerEngine -{ - public partial class ScheduleRule - { - internal ScheduleRule() - { +namespace TaskSchedulerEngine { + public partial class ScheduleRule { + internal ScheduleRule() { // Only used for unit testing } - internal ScheduleRule(TaskEvaluationRuntime runtime) - { + internal ScheduleRule(TaskEvaluationRuntime runtime) { Runtime = runtime; } @@ -30,7 +28,7 @@ internal ScheduleRule(TaskEvaluationRuntime runtime) // Co-authored by Bing/Chat GPT (but it had bugs that I fixed!) internal const string CronRegex = @"^\s*((\*|[0-5]?\d)(,[0-5]?\d)*)\s+((\*|[01]?\d|2[0-3])(,([01]?\d|2[0-3]))*)\s+((\*|0?[0-9]|[12][0-9]|3[01])(,(0?[0-9]|[12][0-9]|3[01]))*)\s+((\*|0?[1-9]|1[012])(,(0?[1-9]|1[012]))*)\s+((\*|[0-6])(,[0-6])*)\s*$"; - + #if NET7_0_OR_GREATER [GeneratedRegex(CronRegex, RegexOptions.IgnoreCase, "en-US")] private static partial Regex CronCompiledRegex(); @@ -41,8 +39,13 @@ internal ScheduleRule(TaskEvaluationRuntime runtime) /// Will always set Seconds=0; call .AtSeconds() to override. Supports comma separated lists of numbers or stars. /// DOES NOT support step (/) or range (-) operators. /// - public ScheduleRule FromCron(string cronExp) - { + public ScheduleRule FromCron(string cronExp) { + + cronExp = cronExp.Replace("\t", " "); + cronExp = cronExp.Replace("\r", " "); + cronExp = cronExp.Replace("\n", " "); + + /* # Example of job definition: @@ -56,33 +59,173 @@ public ScheduleRule FromCron(string cronExp) */ -#if NET7_0_OR_GREATER - var match = CronCompiledRegex().Match(cronExp); -#else - var match = Regex.Match(cronExp, CronRegex); -#endif - if(!match.Success) throw new ArgumentException("Unable to parse cron expression. Only comma-separated digits and * are accepted. Digits must conform to their meaning (e.g. minute must be between 0-59, etc)", "cronExp"); + //#if NET7_0_OR_GREATER + // var match = CronCompiledRegex().Match(cronExp); + //#else + // var match = Regex.Match(cronExp, CronRegex); + //#endif + // if(!match.Success) throw new ArgumentException("Unable to parse cron expression. Only comma-separated digits and * are accepted. Digits must conform to their meaning (e.g. minute must be between 0-59, etc)", "cronExp"); // groups 1 4 8 12 16 - this.Seconds = new int[] { 0 }; - this.Minutes = match.Groups[1].Value == "*" ? new int[] { } : match.Groups[1].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); - this.Hours = match.Groups[4].Value == "*" ? new int[] { } : match.Groups[4].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); - this.DaysOfMonth = match.Groups[8].Value == "*" ? new int[] { } : match.Groups[8].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); - this.Months = match.Groups[12].Value == "*" ? new int[] { } : match.Groups[12].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); - this.DaysOfWeek = match.Groups[16].Value == "*" ? new int[] { } : match.Groups[16].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); - - if(Runtime != null) Runtime.UpdateSchedule(this); + //this.Seconds = new int[] { 0 }; + //this.Minutes = match.Groups[1].Value == "*" ? new int[] { } : match.Groups[1].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); + //this.Hours = match.Groups[4].Value == "*" ? new int[] { } : match.Groups[4].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); + //this.DaysOfMonth = match.Groups[8].Value == "*" ? new int[] { } : match.Groups[8].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); + //this.Months = match.Groups[12].Value == "*" ? new int[] { } : match.Groups[12].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); + //this.DaysOfWeek = match.Groups[16].Value == "*" ? new int[] { } : match.Groups[16].Value.Split(",").Select(s => Int32.Parse(s)).ToArray(); + + var cronParts = cronExp.Split(" "); + if (cronParts.Count() != 5) { + throw new ArgumentException("Cron expression must have 5 parts!"); + } + + this.Seconds = new int[] { 0 }; + this.Minutes = CalculateCronInts(cronParts[0], upperLimit: 59); + this.Hours = CalculateCronInts(cronParts[1], upperLimit: 23); + this.DaysOfMonth = CalculateCronInts(cronParts[2], 31); + this.Months = CalculateCronInts(cronParts[3], 12); + this.DaysOfWeek = CalculateCronInts(cronParts[4], 7); + + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } + + public static int[] CalculateCronInts(string segment, int upperLimit = 59) { + List final_parts = new List(); + List? operandParts = null; + + int? previousInt = null; + int? _currentInnerBuffer = null; + string currentString = ""; + CronOperand? Operand = null; + + Action performOperation = (bool finalCall) => { + if (previousInt == null) + throw new ArgumentException("Operation cannot be called without a leading operand"); + if (_currentInnerBuffer == null) + throw new ArgumentException("Operation cannot be called without a trailing operand"); + switch (Operand) { + + case CronOperand.RANGE: + operandParts = CalculateRange((int)previousInt, (int)_currentInnerBuffer); + break; + + case CronOperand.NTH: + if (operandParts == null) + operandParts = CalculateRange((int)previousInt, upperLimit); + operandParts = CalculateNth(operandParts, (int)_currentInnerBuffer); + break; + + case CronOperand.ADD: + //operandParts = new List() { }; + final_parts.Add((int)previousInt); + if (finalCall) + final_parts.Add((int)_currentInnerBuffer); + break; + + case null: + return; + } + Operand = null; + }; + + Action parseCurrentString = () => { + int _tmp; + if (Int32.TryParse(currentString, out _tmp)) { + _currentInnerBuffer = _tmp; + } else { + _currentInnerBuffer = null; + } + currentString = ""; + }; + + for (int i = 0; i < segment.Length + 1; i++) { + if (i == segment.Length) { + parseCurrentString(); + if (Operand == null) { + if (_currentInnerBuffer != null) + final_parts.Add((int)_currentInnerBuffer); + break; + } + performOperation(true); + if (operandParts != null) + final_parts.AddRange(operandParts!); + break; + } + if (System.Char.IsDigit(segment[i])) { + currentString += segment[i]; + continue; + } else { + parseCurrentString(); + if (Operand != null) { + performOperation(false); + } + switch (segment[i]) { + case '-': + Operand = CronOperand.RANGE; + break; + + case '/': + Operand = CronOperand.NTH; + break; + + case ',': + Operand = CronOperand.ADD; + break; + + case '*': + Operand = null; + break; + + default: + throw new ArgumentException($"Illegal Character {segment[i]} in cron parameter"); + } + previousInt = _currentInnerBuffer ; + + } + } + if (final_parts.Any(x => x > upperLimit)) { + throw new ArgumentException("Upper limit exceeded"); + } + return final_parts.ToArray(); + } + + private static List CalculateRange(int start, int end) { + var ints = new List(); + if (start >= end) + throw new ArgumentException("The start of the range must be less than the end"); + for (int i = start; i < end + 1; i++) { + ints.Add(i); + } + return ints; + } + + public static List CalculateNth(List list, int n) { + if (n <= 0) { + throw new ArgumentException("n must be greater than 0"); + } + List result = new List(); + for (int i = n - 1; i < list.Count; i += n) { + result.Add(list[i]); + } + return result; + } + + + private enum CronOperand { + RANGE, + NTH, + ADD + } + /// /// /// public bool Active { get; private set; } = true; - public ScheduleRule AsActive(bool active) - { + public ScheduleRule AsActive(bool active) { Active = active; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } @@ -93,10 +236,9 @@ public ScheduleRule AsActive(bool active) /// /// /// - public ScheduleRule WithName(string name) - { + public ScheduleRule WithName(string name) { Name = name; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } @@ -108,12 +250,11 @@ public ScheduleRule WithName(string name) internal static readonly int MinYear = DateTimeOffset.Now.Year - 1; public int[] Years { get; private set; } = new int[] { }; - public ScheduleRule AtYears(params int[] value) - { - if(value.Where(v => v < MinYear || v > (MinYear+62)).Count() > 0) - throw new ArgumentOutOfRangeException($"Year must be between {MinYear} and {MinYear+62}."); + public ScheduleRule AtYears(params int[] value) { + if (value.Where(v => v < MinYear || v > (MinYear + 62)).Count() > 0) + throw new ArgumentOutOfRangeException($"Year must be between {MinYear} and {MinYear + 62}."); Years = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } @@ -122,12 +263,11 @@ public ScheduleRule AtYears(params int[] value) /// /// List of months, where 1=Jan, or Empty for any /// - public ScheduleRule AtMonths(params int[] value) - { - if(value.Where(v => v > 12 || v < 1).Count() > 0) + public ScheduleRule AtMonths(params int[] value) { + if (value.Where(v => v > 12 || v < 1).Count() > 0) throw new ArgumentOutOfRangeException("Month must be between 1 (Jan) and 12 (Dec), inclusive."); Months = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } @@ -135,13 +275,12 @@ public ScheduleRule AtMonths(params int[] value) /// /// 1 to 31 or Empty fo any /// - public ScheduleRule AtDaysOfMonth(params int[] value) - { - if(value.Where(v => v > 31 || v < 1).Count() > 0) + public ScheduleRule AtDaysOfMonth(params int[] value) { + if (value.Where(v => v > 31 || v < 1).Count() > 0) throw new ArgumentOutOfRangeException("DaysOfMonth must be between 1 and 31, inclusive."); - + DaysOfMonth = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } @@ -149,13 +288,12 @@ public ScheduleRule AtDaysOfMonth(params int[] value) /// /// 0=Sunday, 1=Mon... 6=Saturday or Empty for any /// - public ScheduleRule AtDaysOfWeek(params int[] value) - { - if(value.Where(v => v > 6 || v < 0).Count() > 0) + public ScheduleRule AtDaysOfWeek(params int[] value) { + if (value.Where(v => v > 6 || v < 0).Count() > 0) throw new ArgumentOutOfRangeException("DaysOfWeek must be between 0 (Sun) and 6 (Sat), inclusive."); - + DaysOfWeek = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } public int[] Hours { get; private set; } = new int[] { }; @@ -163,80 +301,71 @@ public ScheduleRule AtDaysOfWeek(params int[] value) /// 0 (12am, start of the day) to 23 (11pm) /// /// - public ScheduleRule AtHours(params int[] value) - { - if(value.Where(v => v > 23 || v < 0).Count() > 0) + public ScheduleRule AtHours(params int[] value) { + if (value.Where(v => v > 23 || v < 0).Count() > 0) throw new ArgumentOutOfRangeException("Hours must be between 0 (12am, start of day) and 23 (11pm), inclusive"); - + Hours = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } public int[] Minutes { get; private set; } = new int[] { }; /// /// 0 to 59 /// - public ScheduleRule AtMinutes(params int[] value) - { - if(value.Where(v => v > 59 || v < 0).Count() > 0) + public ScheduleRule AtMinutes(params int[] value) { + if (value.Where(v => v > 59 || v < 0).Count() > 0) throw new ArgumentOutOfRangeException("Minutes must be between 0 and 59, inclusive"); - + Minutes = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } public int[] Seconds { get; private set; } = new int[] { }; /// /// 0 to 59 /// - public ScheduleRule AtSeconds(params int[] value) - { - if(value.Where(v => v > 59 || v < 0).Count() > 0) + public ScheduleRule AtSeconds(params int[] value) { + if (value.Where(v => v > 59 || v < 0).Count() > 0) throw new ArgumentOutOfRangeException("Seconds must be between 0 and 59, inclusive"); - + Seconds = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } public TimeZoneInfo TimeZone { get; private set; } = TimeZoneInfo.Utc; - public ScheduleRule WithUtc() - { + public ScheduleRule WithUtc() { TimeZone = TimeZoneInfo.Utc; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } - public ScheduleRule WithLocalTime() - { + public ScheduleRule WithLocalTime() { TimeZone = TimeZoneInfo.Local; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } /// /// Looks up time zone from the system. /// /// Rules defined here https://learn.microsoft.com/en-us/dotnet/api/system.timezoneinfo.findsystemtimezonebyid - public ScheduleRule WithTimeZone(string tzId) - { + public ScheduleRule WithTimeZone(string tzId) { try { return WithTimeZone(TimeZoneInfo.FindSystemTimeZoneById(tzId)); - } - catch (TimeZoneNotFoundException) { + } catch (TimeZoneNotFoundException) { throw new ArgumentException($"System cannot find time zone {tzId}.", nameof(tzId)); } } - public ScheduleRule WithTimeZone(TimeZoneInfo tz) - { + public ScheduleRule WithTimeZone(TimeZoneInfo tz) { TimeZone = tz; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } /// /// Execute once at the floor of the second specified. Shorthand for setting Year, Month, DayOfMonth, Second, and Expiration. /// - public ScheduleRule ExecuteOnceAt(DateTimeOffset time) - { + public ScheduleRule ExecuteOnceAt(DateTimeOffset time) { time = time.ToUniversalTime(); var s = this.AtYears(time.Year) .AtMonths(time.Month) @@ -244,7 +373,7 @@ public ScheduleRule ExecuteOnceAt(DateTimeOffset time) .AtSeconds(time.Second) .WithUtc() .ExpiresAfter(time.AddMinutes(1)); - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return s; } @@ -255,8 +384,7 @@ public ScheduleRule ExecuteOnceAt(DateTimeOffset time) /// /// /// - public ScheduleRule ExecuteEveryYear(params int[] value) - { + public ScheduleRule ExecuteEveryYear(params int[] value) { return this.AtYears(value) .AtMonths(1) .AtDaysOfMonth(1) @@ -270,8 +398,7 @@ public ScheduleRule ExecuteEveryYear(params int[] value) /// /// /// - public ScheduleRule ExecuteEveryMonth(params int[] value) - { + public ScheduleRule ExecuteEveryMonth(params int[] value) { return this .AtMonths(value) .AtDaysOfMonth(1) @@ -286,8 +413,7 @@ public ScheduleRule ExecuteEveryMonth(params int[] value) /// /// /// - public ScheduleRule ExecuteEveryDayOfMonth(params int[] value) - { + public ScheduleRule ExecuteEveryDayOfMonth(params int[] value) { return this .AtDaysOfMonth(value) .AtHours(0) @@ -300,8 +426,7 @@ public ScheduleRule ExecuteEveryDayOfMonth(params int[] value) /// /// /// - public ScheduleRule ExecuteEveryDayOfWeek(params int[] value) - { + public ScheduleRule ExecuteEveryDayOfWeek(params int[] value) { return this.AtDaysOfWeek(value) .AtHours(0) .AtMinutes(0) @@ -313,8 +438,7 @@ public ScheduleRule ExecuteEveryDayOfWeek(params int[] value) /// /// /// - public ScheduleRule ExecuteEveryHour(params int[] value) - { + public ScheduleRule ExecuteEveryHour(params int[] value) { return this.AtHours(value) .AtMinutes(0) .AtSeconds(0); @@ -325,8 +449,7 @@ public ScheduleRule ExecuteEveryHour(params int[] value) /// /// /// - public ScheduleRule ExecuteEveryMinute(params int[] value) - { + public ScheduleRule ExecuteEveryMinute(params int[] value) { return this.AtMinutes(value) .AtSeconds(0); } @@ -336,9 +459,8 @@ public ScheduleRule ExecuteEveryMinute(params int[] value) /// /// /// - public ScheduleRule ExecuteEverySecond(params int[] value) - { - return this.AtSeconds(value ?? Enumerable.Range(0,60).ToArray()); + public ScheduleRule ExecuteEverySecond(params int[] value) { + return this.AtSeconds(value ?? Enumerable.Range(0, 60).ToArray()); } @@ -348,10 +470,9 @@ public ScheduleRule ExecuteEverySecond(params int[] value) /// If not deleted, ongoing added schedules (e.g. in retry scenario) will live forever /// and memory leak. /// - public ScheduleRule ExpiresAfter(DateTimeOffset value) - { + public ScheduleRule ExpiresAfter(DateTimeOffset value) { Expiration = value; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } @@ -360,37 +481,32 @@ public ScheduleRule ExpiresAfter(DateTimeOffset value) /// /// Execute a task with exponential backoff algorithm. See ExponentialBackoffTask for details. /// - public ScheduleRule ExecuteAndRetry(Func> callback, int maxAttempts, int baseRetryIntervalSeconds) - { + public ScheduleRule ExecuteAndRetry(Func> callback, int maxAttempts, int baseRetryIntervalSeconds) { Task = new ExponentialBackoffTask(callback, maxAttempts, baseRetryIntervalSeconds); - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } /// /// Execute a task with exponential backoff algorithm. See ExponentialBackoffTask for details. /// - public ScheduleRule ExecuteAndRetry(Func callback, int maxAttempts, int baseRetryIntervalSeconds) - { + public ScheduleRule ExecuteAndRetry(Func callback, int maxAttempts, int baseRetryIntervalSeconds) { return ExecuteAndRetry(async (e, ct) => callback(e, ct), maxAttempts, baseRetryIntervalSeconds); } - public ScheduleRule Execute(Func> callback) - { + public ScheduleRule Execute(Func> callback) { Task = new AnonymousScheduledTask(callback); - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } - public ScheduleRule Execute(Func callback) - { + public ScheduleRule Execute(Func callback) { return Execute(async (e, ct) => callback(e, ct)); } - public ScheduleRule Execute(IScheduledTask taskInstance) - { + public ScheduleRule Execute(IScheduledTask taskInstance) { Task = taskInstance; - if(Runtime != null) Runtime.UpdateSchedule(this); + if (Runtime != null) Runtime.UpdateSchedule(this); return this; } @@ -398,8 +514,7 @@ public ScheduleRule Execute(IScheduledTask taskInstance) /// Print debugging info about this schedule /// /// - public override string ToString() - { + public override string ToString() { return @$"Schedule has name {Name}. Rule: Seconds: {PrintArr(Seconds)} Minutes: {PrintArr(Minutes)} @@ -413,14 +528,12 @@ public override string ToString() "; } - private string PrintArr(int[] val) - { - if(val == null) return "* (null)"; - if(val.Length == 0) return "* (empty)"; - + private string PrintArr(int[] val) { + if (val == null) return "* (null)"; + if (val.Length == 0) return "* (empty)"; + string m = ""; - foreach (var i in val) - { + foreach (var i in val) { m = m + i.ToString() + ","; } return m.TrimEnd(','); diff --git a/test/CronExtendedFunctionality.cs b/test/CronExtendedFunctionality.cs new file mode 100644 index 0000000..981622b --- /dev/null +++ b/test/CronExtendedFunctionality.cs @@ -0,0 +1,185 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TaskSchedulerEngine; + +namespace TaskSchedulerEngineTests { + [TestClass] + public class CronExtendedFunctionality { + public CronExtendedFunctionality() { } + + [TestMethod("Simple Range")] + public void Range1() { + var expression = "0-3"; + var expected = new int[] { 0, 1, 2, 3 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Multi Digit Range")] + public void Range3() { + var expression = "0-12"; + var expected = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Simple Range 2")] + public void Range2() { + var expression = "0-5"; + var expected = new int[] { 0, 1, 2, 3, 4, 5 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression); + CollectionAssert.AreEqual(expected, results); + } + + + [TestMethod("Range with Nth Selector")] + public void RangeWithNth1() { + var expression = "0-4/2"; + var expected = new int[] { 1, 3 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Multi Digit Range with Nth Selector 1")] + public void RangeWithNth2() { + var expression = "0-12/2"; + var expected = new int[] { 1, 3, 5, 7, 9, 11 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Nth Selector")] + public void Nth1() { + var expression = "50/1"; + var expected = new int[] { 50, 51, 52, 53, 54, 55, 56, 57, 58, 59 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Nth Selector 3")] + public void Nth3() { + var expression = "0,1/20"; + var expected = new int[] { 0, 20, 40 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Nth Selector 2")] + public void Nth2() { + var expression = "50/2"; + var expected = new int[] { 51, 53, 55, 57, 59 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Multi Digit Nth Selector")] + public void MultiDigitNth() { + var expression = "1/10"; + var expected = new int[] { 10, 20, 30, 40, 50 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Simple List")] + public void List1() { + var expression = "1,2,3,4"; + var expected = new int[] { 1, 2, 3, 4, }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Mixed Test 1")] + public void MixedTest1() { + var expression = "1,2,3,4-10"; + var expected = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 , 10}; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Mixed Test 2")] + public void MixedTest2() { + var expression = "20"; + var expected = new int[] { 20 }; + var results = TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod("Full Cron 1")] + public void CronFull1() { + var rule = new ScheduleRule() + .FromCron("0 2-3 * * *") + .Execute((a, b) => true); + } + + [TestMethod("Full Cron 2")] + public void CronFull2() { + var rule = new ScheduleRule() + .FromCron("0 1/1 * * *") + .Execute((a, b) => true); + } + + + [TestMethod("Full Cron 3")] + public void CronFull3() { + var rule = new ScheduleRule() + .FromCron("59 * * * *") + .Execute((a, b) => true); + } + + // FAILIURES + + [TestMethod("Fail 1")] + [ExpectedException(typeof(ArgumentException), "Expression with leading operand passed!")] + public void Fail1() { + var expression = ",1,2,3,4"; + TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + } + + [TestMethod("Fail 2")] + [ExpectedException(typeof(ArgumentException), "Expression with leading operand passed!")] + public void Fail2() { + var expression = "-3"; + TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + } + + [TestMethod("Fail 3")] + [ExpectedException(typeof(ArgumentException), "Expression with leading operand passed!")] + public void Fail3() { + var expression = "0,-3"; + TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + } + + [TestMethod("Fail 4")] + [ExpectedException(typeof(ArgumentException), "Expression with trailing operand passed!")] + public void Fail4() { + var expression = "1,2,3,4,"; + TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + } + + [TestMethod("Fail 5")] + [ExpectedException(typeof(ArgumentException), "Expression with trailing operand passed!")] + public void Fail5() { + var expression = "3-"; + TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + } + + [TestMethod("Fail 6")] + [ExpectedException(typeof(ArgumentException), "Expression with trailing operand passed!")] + public void Fail6() { + var expression = "0,3-"; + TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + } + + [TestMethod("Fail 7")] + [ExpectedException(typeof(ArgumentException), "Expression with invalid character!")] + public void Fail7() { + var expression = "4(4"; + TaskSchedulerEngine.ScheduleRule.CalculateCronInts(expression, 59); + } + + } +} diff --git a/test/ScheduleRuleTest.cs b/test/ScheduleRuleTest.cs index c39583c..ba3a543 100644 --- a/test/ScheduleRuleTest.cs +++ b/test/ScheduleRuleTest.cs @@ -438,5 +438,6 @@ public void TimeZoneTests() Assert.IsTrue(new ScheduleEvaluationOptimized(rule).EvaluateRuleMatch(new DateTimeOffset(DateTime.Now.Year, 6, 19, 11, 0, 0, TimeSpan.FromHours(-7)))); Assert.IsFalse(new ScheduleEvaluationOptimized(rule).EvaluateRuleMatch(new DateTimeOffset(DateTime.Now.Year, 6, 19, 11, 0, 0, TimeSpan.Zero))); } + } } \ No newline at end of file