using System; using System.Collections.Generic; using System.Globalization; namespace Microsoft.Win32.TaskScheduler { public abstract partial class Trigger { /// /// Creates a trigger using a cron string. /// /// String using cron defined syntax for specifying a time interval. See remarks for syntax. /// Array of representing the specified cron string. /// Unsupported cron string. /// /// NOTE: This method does not support all combinations of cron strings. Please test extensively before use. Please post an issue with any syntax that should work, but doesn't. /// Currently the cronString only supports numbers and not any of the weekday or month strings. Please use numeric equivalent. /// This section borrows liberally from the site http://www.nncron.ru/help/EN/working/cron-format.htm. The cron format consists of five fields separated by white spaces: /// /// <Minute> <Hour> <Day_of_the_Month> <Month_of_the_Year> <Day_of_the_Week> /// /// Each item has bounds as defined by the following: /// /// * * * * * /// | | | | | /// | | | | +---- Day of the Week (range: 1-7, 1 standing for Monday) /// | | | +------ Month of the Year (range: 1-12) /// | | +-------- Day of the Month (range: 1-31) /// | +---------- Hour (range: 0-23) /// +------------ Minute (range: 0-59) /// /// Any of these 5 fields may be an asterisk (*). This would mean the entire range of possible values, i.e. each minute, each hour, etc. /// Any of the first 4 fields can be a question mark ("?"). It stands for the current time, i.e. when a field is processed, the current time will be substituted for the question mark: minutes for Minute field, hour for Hour field, day of the month for Day of month field and month for Month field. /// Any field may contain a list of values separated by commas, (e.g. 1,3,7) or a range of values (two integers separated by a hyphen, e.g. 1-5). /// After an asterisk (*) or a range of values, you can use character / to specify that values are repeated over and over with a certain interval between them. For example, you can write "0-23/2" in Hour field to specify that some action should be performed every two hours (it will have the same effect as "0,2,4,6,8,10,12,14,16,18,20,22"); value "*/4" in Minute field means that the action should be performed every 4 minutes, "1-30/3" means the same as "1,4,7,10,13,16,19,22,25,28". /// public static Trigger[] FromCronFormat(string cronString) { CronExpression cron = new CronExpression(); cron.Parse(cronString); // TODO: Figure out all the permutations of expression and convert to Trigger(s) /* Time (fields 1-4 have single number and dow = *) * Time repeating * Daily * Weekly * Monthly * Monthly DOW */ List ret = new List(); // MonthlyDOWTrigger if (!cron.DOW.IsEvery) { // Determine DOW DaysOfTheWeek dow = 0; if (cron.DOW.vals.Length == 0) dow = DaysOfTheWeek.AllDays; else if (cron.DOW.range) for (int i = cron.DOW.vals[0]; i <= cron.DOW.vals[1]; i += cron.DOW.step) dow |= (DaysOfTheWeek)(1 << (i - 1)); else for (int i = 0; i < cron.DOW.vals.Length; i++) dow |= (DaysOfTheWeek)(1 << (cron.DOW.vals[i] - 1)); // Determine months MonthsOfTheYear moy = 0; if ((cron.Months.vals.Length == 0 || (cron.Months.vals.Length == 1 && cron.Months.vals[0] == 1)) && cron.Months.IsEvery) moy = MonthsOfTheYear.AllMonths; else if (cron.Months.range) for (int i = cron.Months.vals[0]; i <= cron.Months.vals[1]; i += cron.Months.step) moy |= (MonthsOfTheYear)(1 << (i - 1)); else for (int i = 0; i < cron.Months.vals.Length; i++) moy |= (MonthsOfTheYear)(1 << (cron.Months.vals[i] - 1)); Trigger tr = new MonthlyDOWTrigger(dow, moy, WhichWeek.AllWeeks); ret.AddRange(ProcessCronTimes(cron, tr)); } // MonthlyTrigger else if (cron.Days.vals.Length > 0) { // Determine DOW List days = new List(); if (cron.Days.range) for (int i = cron.Days.vals[0]; i <= cron.Days.vals[1]; i += cron.Days.step) days.Add(i); else for (int i = 0; i < cron.Days.vals.Length; i++) days.Add(cron.Days.vals[i]); // Determine months MonthsOfTheYear moy = 0; if ((cron.Months.vals.Length == 0 || (cron.Months.vals.Length == 1 && cron.Months.vals[0] == 1)) && cron.Months.IsEvery) moy = MonthsOfTheYear.AllMonths; else if (cron.Months.range) for (int i = cron.Months.vals[0]; i <= cron.Months.vals[1]; i += cron.Months.step) moy |= (MonthsOfTheYear)(1 << (i - 1)); else for (int i = 0; i < cron.Months.vals.Length; i++) moy |= (MonthsOfTheYear)(1 << (cron.Months.vals[i] - 1)); Trigger tr = new MonthlyTrigger(1, moy) { DaysOfMonth = days.ToArray() }; ret.AddRange(ProcessCronTimes(cron, tr)); } // DailyTrigger else if (cron.Months.IsEvery && cron.DOW.IsEvery && cron.Days.repeating) { Trigger tr = new DailyTrigger((short)cron.Days.step); ret.AddRange(ProcessCronTimes(cron, tr)); } else { throw new NotImplementedException(); } return ret.ToArray(); } private static IEnumerable ProcessCronTimes(CronExpression cron, Trigger baseTrigger) { List ret = new List(); // A single time if (cron.Minutes.vals.Length == 1 && cron.Hours.vals.Length == 1) { baseTrigger.StartBoundary = baseTrigger.StartBoundary.Date + new TimeSpan(cron.Hours.vals[0], cron.Minutes.vals[0], 0); ret.Add(baseTrigger); } // Multiple, non-repeating, hours and/or minutes else if (cron.Minutes.vals.Length > 1 && !cron.Minutes.range && cron.Hours.vals.Length > 1 && !cron.Hours.range) { for (int h = 0; h < cron.Hours.vals.Length; h++) { for (int m = 0; m < cron.Minutes.vals.Length; m++) { Trigger newTr = (Trigger)baseTrigger.Clone(); newTr.StartBoundary = newTr.StartBoundary.Date + new TimeSpan(cron.Hours.vals[h], cron.Minutes.vals[m], 0); ret.Add(newTr); } } } // Repeating hours and/or minutes else if (cron.Minutes.step > 0 || cron.Hours.step > 0) { int h_start = 0, h_end = 23, m_start = 0, m_end = 59; if (cron.Minutes.range) { m_start = cron.Minutes.vals[0]; m_end = cron.Minutes.vals[1]; } else if (cron.Minutes.vals.Length == 1) m_start = m_end = cron.Minutes.vals[0]; if (cron.Hours.range) { h_start = cron.Hours.vals[0]; h_end = cron.Hours.vals[1]; } else if (cron.Hours.vals.Length == 1) h_start = h_end = cron.Hours.vals[0]; if (h_start == h_end) { Trigger newTr = (Trigger)baseTrigger.Clone(); newTr.StartBoundary = newTr.StartBoundary.Date + new TimeSpan(h_start, m_start, 0); newTr.Repetition.Interval = TimeSpan.FromMinutes(cron.Minutes.step); newTr.Repetition.Duration = TimeSpan.FromHours(1); ret.Add(newTr); } else if (m_start == m_end) { Trigger newTr = (Trigger)baseTrigger.Clone(); newTr.StartBoundary = newTr.StartBoundary.Date + new TimeSpan(h_start, m_start, 0); newTr.Repetition.Interval = TimeSpan.FromHours(cron.Hours.step); newTr.Repetition.Duration = TimeSpan.FromHours(h_end - h_start); ret.Add(newTr); } else { throw new NotImplementedException(); } } return ret; } private class CronExpression { private FieldVal[] Fields = new FieldVal[5]; public CronExpression() { } public void Parse(string cronString) { if (cronString == null) throw new ArgumentNullException("cronString"); var tokens = cronString.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (tokens.Length != 5) { throw new ArgumentException(string.Format("'{0}' is not a valid crontab expression. It must contain at least 5 components of a schedule " + "(in the sequence of minutes, hours, days, months, days of week).", cronString)); } // min, hr, days, months, daysOfWeek for (var i = 0; i < Fields.Length; i++) { Fields[i] = ParseCronField(tokens[i], (CronFieldType)i); } } public FieldVal Minutes { get { return Fields[0]; } } public FieldVal Hours { get { return Fields[1]; } } public FieldVal Days { get { return Fields[2]; } } public FieldVal Months { get { return Fields[3]; } } public FieldVal DOW { get { return Fields[4]; } } private FieldVal ParseCronField(string str, CronFieldType cft) { FieldVal res = new FieldVal(); if (string.IsNullOrEmpty(str)) throw new ArgumentNullException("A crontab field value cannot be empty."); // Look first for a list of values (e.g. 1,2,3). if (str.IndexOf(",") > 0) { res.vals = Array.ConvertAll(str.Split(','), delegate(string s) { return ParseInt(s); }); res.Validate(cft); return res; } // Look for stepping (e.g. */2 = every 2nd). res.step = 1; var slashIndex = str.IndexOf("/"); if (slashIndex > 0) { res.step = ParseInt(str.Substring(slashIndex + 1)); str = str.Substring(0, slashIndex); } // Next, look for wildcard (*). if (str.Length == 1 && str[0] == '*') { res.vals = new int[0]; res.repeating = true; return res; } // Next, look for a range of values (e.g. 2-10). var dashIndex = str.IndexOf("-"); if (dashIndex > 0) { var first = ParseInt(str.Substring(0, dashIndex)); var last = ParseInt(str.Substring(dashIndex + 1)); if (first >= last) throw new ArgumentException("A crontab field value range must begin with a lower number"); res.range = true; res.vals = new int[] { first, last }; res.Validate(cft); return res; } // Check for "?" and substitute current time if (str.Length == 1 && str[0] == '?') { DateTime now = DateTime.Now; int nval = 0; switch (cft) { case CronFieldType.Minutes: nval = now.Minute; break; case CronFieldType.Hours: nval = now.Hour; break; case CronFieldType.Days: nval = now.Day; break; case CronFieldType.Months: nval = now.Month; break; case CronFieldType.DaysOfWeek: throw new ArgumentException("The fifth parameter representing the day of the week cannot be a '?'."); default: break; } res.vals = new int[] { nval }; res.step = 0; res.Validate(cft); return res; } // Finally, handle the case where there is only one number. var value = ParseInt(str); res.vals = new int[] { value }; res.step = 0; res.Validate(cft); return res; } private static int ParseInt(string str) { return int.Parse(str.Trim()); } public enum CronFieldType { Minutes, Hours, Days, Months, DaysOfWeek }; public struct FieldVal { public int[] vals; public bool repeating, range; public int step; public bool IsEvery { get { return step == 1 && repeating; } } public bool Validate(CronFieldType cft) { switch (cft) { case CronFieldType.Minutes: return Array.TrueForAll(vals, delegate(int i) { return i >= 0 && i <= 59; }); case CronFieldType.Hours: return Array.TrueForAll(vals, delegate(int i) { return i >= 0 && i <= 23; }); case CronFieldType.Days: return Array.TrueForAll(vals, delegate(int i) { return i >= 1 && i <= 31; }); case CronFieldType.Months: return Array.TrueForAll(vals, delegate(int i) { return i >= 1 && i <= 12; }); case CronFieldType.DaysOfWeek: return Array.TrueForAll(vals, delegate(int i) { return i >= 0 && i <= 6; }); default: break; } return false; } } } } }