It’s been a while since I have had anything to say and that’s mostly because of how busy I’ve been recently. In that time though, I have posted a number of my frameworks on NuGet located here. And to complement those frameworks, I have started to write the SandCastle documentation so that users can understand how my methods work. So, with all that in mind, I now have plently of material to blog about.
Today, we are going to talk briefly about scheduling. Or, more importantly, how we can write code that will provide a set of dates to complement a variety of date ranges or number of desired occurrences within a given time frame. To best understand this we should look at pulling a set of dates by days, weeks, months, and years. So, you will see a lot of similar patterns.
Before I get into it, let me talk about the difference between evaluating dates By Schedule vs By Occurrence.
By Schedule is concerned with: a start date, and an end date. This is because we are looking for the number of occurrences that happen between 2 static dates. Ex. Run this script every Tuesday until 2/20/2020.
By Occurrence is a little different. Instead it cares about start date, number of occurrences, and a maximum number of years to allow as an upper range. This is because here, we are looking for the dates that result from a specified number of occurrences. Ex. run this script up to 60 times within the next 5 years.
Assessing Dates By Days
To schedule something in terms of days, the most important variable to consider is the number of days between each instance. Essentially, we just need to enumerate through the dates by the number of days until we hit the end date or the number of occurrences depending if we query by schedule or by occurrence.
The source code would look something like this:
/// <summary> /// Gets a list of valid dates related to days within a scheduled range. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="endDate">The end date.</param> /// <param name="numberOfDays">The number of days to evaluate.</param> /// <returns></returns> public static DateTime[] GetValidDaysBySchedule( DateTime startDate, DateTime endDate, int numberOfDays) { // List to return var list = new List<DateTime>(); // Set first date to evaluate to the start date var currentDate = startDate; // Must have a valid start and end date, and the end date must be later in time than the // start date. if (startDate == default(DateTime) || endDate == default(DateTime) || !(endDate >= startDate)) return list.ToArray(); // Process // 1) Do not exceed the end date, and while (endDate.Subtract(currentDate).TotalDays >= 0) { // Add date list.Add(currentDate); // Get next date currentDate = currentDate.AddDays(numberOfDays); } return list.ToArray(); } /// <summary> /// Gets a list of valid dates related to days within a number of occurrences. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="numberOfDays">The number of days.</param> /// <param name="numberOfOccurrences">The number of occurrences. The default is 1.</param> /// <param name="maximumYearsAllowed">The maximum years allowed. The default is 10.</param> /// <returns></returns> public static DateTime[] GetValidDaysByOccurrence( DateTime startDate, int numberOfDays, int numberOfOccurrences = 1, int maximumYearsAllowed = 1) { // List to return var list = new List<DateTime>(); // Set first date to evaluate to the start date var currentDate = startDate; // Must have a valid start and end date, and the occurrence count must not be exceeded. if (startDate == default(DateTime) || list.Count >= numberOfOccurrences) return list.ToArray(); // Process // 1) Do not exceed occurrence count, and // 2) do not exceed the maximum allowed years in a date range. var maxDate = startDate.AddYears(maximumYearsAllowed); while (list.Count < numberOfOccurrences && maxDate >= currentDate) { // Add date list.Add(currentDate); // Get next date currentDate = currentDate.AddDays(numberOfDays); } return list.ToArray(); }
An example of how to use this in a console application:
// Main static void Main(string[] args) { var startDate = new DateTime(2018, 2, 17); var endDate = startDate.AddMonths(4); // Occurs every 10 days up to the end date Console.WriteLine("By date range:"); var dates = ScheduleHelper.GetValidDaysBySchedule(startDate, endDate, 10); foreach (var date in dates) { Console.WriteLine($"{date:MM/dd/yyyy, dddd}"); } // Spacer Console.WriteLine(""); // Occurs every 10 days, up to 10 times, not to exceed 1 year Console.WriteLine("By # of occurrences:"); var dates2 = ScheduleHelper.GetValidDaysByOccurrence(startDate, 10, 10, 1); foreach (var date in dates2) { Console.WriteLine($"{date:MM/dd/yyyy, dddd}"); } // Hold Console.Read(); } /* Result: * * By date range: * 02/17/2018, Saturday * 02/27/2018, Tuesday * 03/09/2018, Friday * 03/19/2018, Monday * 03/29/2018, Thursday * 04/08/2018, Sunday * 04/18/2018, Wednesday * 04/28/2018, Saturday * 05/08/2018, Tuesday * 05/18/2018, Friday * 05/28/2018, Monday * 06/07/2018, Thursday * 06/17/2018, Sunday * * By # of occurrences: * 02/17/2018, Saturday * 02/27/2018, Tuesday * 03/09/2018, Friday * 03/19/2018, Monday * 03/29/2018, Thursday * 04/08/2018, Sunday * 04/18/2018, Wednesday * 04/28/2018, Saturday * 05/08/2018, Tuesday * 05/18/2018, Friday */
Assessing Dates By Weeks
To schedule something in terms of weeks, we not only need to look at the number of weeks between each occurrence, we also need to specify which days of the week are relevant. Performing evaluations of weeks are in my opinion the most complex of the 4 types.
The source code would look something like this:
/// <summary> /// Days of the week /// </summary> public enum DaysOfTheWeek { /// <summary> /// Sunday /// </summary> Sunday = 0, /// <summary> /// Monday /// </summary> Monday = 1, /// <summary> /// Tuesday /// </summary> Tuesday = 2, /// <summary> /// Wednesday /// </summary> Wednesday = 3, /// <summary> /// Thursday /// </summary> Thursday = 4, /// <summary> /// Friday /// </summary> Friday = 5, /// <summary> /// Saturday /// </summary> Saturday = 6 } /// <summary> /// Gets a list of valid dates related to weeks within a scheduled range. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="endDate">The end date.</param> /// <param name="numberOfWeeks">The number of weeks to skip per block of selected days.</param> /// <param name="selectedDays">The selected days of the week the event occurs.</param> /// <returns></returns> public static DateTime[] GetValidWeeksBySchedule( DateTime startDate, DateTime endDate, int numberOfWeeks, DaysOfTheWeek[] selectedDays) { // List to return var list = new List<DateTime>(); // Get the start of the week var dayOfWeek = (int)startDate.DayOfWeek; // Set first date to evaluate to the start date var currentDate = startDate; // Get selected days and convert to their integer values // If no days were specified, then exit. var validDays = selectedDays.Select(x => (int)x).ToArray(); if (!validDays.Any()) return list.ToArray(); // Must have a valid start and end date, and // the end date must be later in time than the start date. if (startDate == default(DateTime) || endDate == default(DateTime) || !(endDate >= startDate)) { return list.ToArray(); } // Process // 1) Do not exceed the end date while (endDate.Subtract(currentDate).TotalDays >= 0) { // Get the day of the week (value) var currentDayOfWeek = (int)currentDate.DayOfWeek; // Evaluate against selected says of the week if (validDays.Contains(currentDayOfWeek)) list.Add(currentDate); // Evaluate the next day var nextDay = currentDate.AddDays(1); // If the day of the week is not the original, and number of weeks is no greater than 1 // Set the date as the next day. Otherwise, jump by the number of weeks specified. if ((int)nextDay.DayOfWeek != dayOfWeek || numberOfWeeks <= 1) { currentDate = nextDay; } else { var interval = 7 * (numberOfWeeks - 1); currentDate = nextDay.AddDays(interval); } } // Return return list.ToArray(); } /// <summary> /// Gets a list of valid dates related to weeks within a number of occurrences. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="numberOfWeeks">The number of weeks to skip per block of selected days.</param> /// <param name="selectedDays">The selected days of the week the event occurs.</param> /// <param name="numberOfOccurrences">The number of occurrences. The default is 1.</param> /// <param name="maximumYearsAllowed">The maximum years allowed. The default is 10.</param> /// <returns></returns> public static DateTime[] GetValidWeeksByOccurrence( DateTime startDate, int numberOfWeeks, DaysOfTheWeek[] selectedDays, int numberOfOccurrences = 1, int maximumYearsAllowed = 10) { // List to return var list = new List<DateTime>(); // Get the start of the week var dayOfWeek = (int)startDate.DayOfWeek; // Set first date to evaluate to the start date var currentDate = startDate; // Get selected days and convert to their integer values // If no days were specified, then exit. var validDays = selectedDays.Select(x => (int)x).ToArray(); if (!validDays.Any()) return list.ToArray(); // Must have a valid start and end date, and // the occurrence count must not be exceeded. if (startDate == default(DateTime) || list.Count >= numberOfOccurrences) { return list.ToArray(); } // Process // 1) Do not exceed occurrence, and // 2) do not exceed the maximum allowed years in a date // range. var maxDate = startDate.AddYears(maximumYearsAllowed); while (list.Count < numberOfOccurrences && maxDate >= currentDate) { // Get the day of the week (value) var currentDayOfWeek = (int)currentDate.DayOfWeek; // Evaluate against selected says of the week if (validDays.Contains(currentDayOfWeek)) list.Add(currentDate); // Evaluate the next day var nextDay = currentDate.AddDays(1); // If the day of the week is not the original, and number of weeks is no greater than 1 // Set the date as the next day. Otherwise, jump by the number of weeks specified. if ((int)nextDay.DayOfWeek != dayOfWeek || numberOfWeeks <= 1) { currentDate = nextDay; } else { var interval = 7 * (numberOfWeeks - 1); currentDate = nextDay.AddDays(interval); } } // Return return list.ToArray(); }
An example of how to use this in a console application:
// Main static void Main(string[] args) { var startDate = new DateTime(2018, 2, 17); var endDate = startDate.AddYears(1); // Occurs every 2 weeks on Sunday, Monday, and Thursday not to exceed the end date. var dates = ScheduleHelper.GetValidWeeksBySchedule(startDate, endDate, 2, new[] { DaysOfTheWeek.Sunday, DaysOfTheWeek.Monday, DaysOfTheWeek.Thursday }); Console.WriteLine($"By date range ({dates.Length}):"); foreach (var date in dates) { Console.WriteLine($"{date:MM/dd/yyy, dddd}"); } // Spacer Console.WriteLine(""); // Occurs up to 30 times on Sunday, Monday, and Thursday no longer than 1 year. var dates2 = ScheduleHelper.GetValidWeeksByOccurrence(startDate, 1, new[] { Enums.DaysOfTheWeek.Sunday, Enums.DaysOfTheWeek.Monday, Enums.DaysOfTheWeek.Thursday }, 30, 1); Console.WriteLine($"By # of occurrences ({dates2.Length}):"); foreach (var date in dates2) { Console.WriteLine($"{date:MM/dd/yyyy, dddd}"); } // Hold Console.Read(); } /* Result: * * By date range (79): * 02/18/2018, Sunday * 02/19/2018, Monday * 02/22/2018, Thursday * 03/04/2018, Sunday * 03/05/2018, Monday * 03/08/2018, Thursday * 03/18/2018, Sunday * 03/19/2018, Monday * 03/22/2018, Thursday * 04/01/2018, Sunday * 04/02/2018, Monday * 04/05/2018, Thursday * 04/15/2018, Sunday * 04/16/2018, Monday * 04/19/2018, Thursday * 04/29/2018, Sunday * 04/30/2018, Monday * 05/03/2018, Thursday * 05/13/2018, Sunday * 05/14/2018, Monday * 05/17/2018, Thursday * 05/27/2018, Sunday * 05/28/2018, Monday * 05/31/2018, Thursday * 06/10/2018, Sunday * 06/11/2018, Monday * 06/14/2018, Thursday * 06/24/2018, Sunday * 06/25/2018, Monday * 06/28/2018, Thursday * 07/08/2018, Sunday * 07/09/2018, Monday * 07/12/2018, Thursday * 07/22/2018, Sunday * 07/23/2018, Monday * 07/26/2018, Thursday * 08/05/2018, Sunday * 08/06/2018, Monday * 08/09/2018, Thursday * 08/19/2018, Sunday * 08/20/2018, Monday * 08/23/2018, Thursday * 09/02/2018, Sunday * 09/03/2018, Monday * 09/06/2018, Thursday * 09/16/2018, Sunday * 09/17/2018, Monday * 09/20/2018, Thursday * 09/30/2018, Sunday * 10/01/2018, Monday * 10/04/2018, Thursday * 10/14/2018, Sunday * 10/15/2018, Monday * 10/18/2018, Thursday * 10/28/2018, Sunday * 10/29/2018, Monday * 11/01/2018, Thursday * 11/11/2018, Sunday * 11/12/2018, Monday * 11/15/2018, Thursday * 11/25/2018, Sunday * 11/26/2018, Monday * 11/29/2018, Thursday * 12/09/2018, Sunday * 12/10/2018, Monday * 12/13/2018, Thursday * 12/23/2018, Sunday * 12/24/2018, Monday * 12/27/2018, Thursday * 01/06/2019, Sunday * 01/07/2019, Monday * 01/10/2019, Thursday * 01/20/2019, Sunday * 01/21/2019, Monday * 01/24/2019, Thursday * 02/03/2019, Sunday * 02/04/2019, Monday * 02/07/2019, Thursday * 02/17/2019, Sunday * * By # of occurrences (30): * 02/18/2018, Sunday * 02/19/2018, Monday * 02/22/2018, Thursday * 02/25/2018, Sunday * 02/26/2018, Monday * 03/01/2018, Thursday * 03/04/2018, Sunday * 03/05/2018, Monday * 03/08/2018, Thursday * 03/11/2018, Sunday * 03/12/2018, Monday * 03/15/2018, Thursday * 03/18/2018, Sunday * 03/19/2018, Monday * 03/22/2018, Thursday * 03/25/2018, Sunday * 03/26/2018, Monday * 03/29/2018, Thursday * 04/01/2018, Sunday * 04/02/2018, Monday * 04/05/2018, Thursday * 04/08/2018, Sunday * 04/09/2018, Monday * 04/12/2018, Thursday * 04/15/2018, Sunday * 04/16/2018, Monday * 04/19/2018, Thursday * 04/22/2018, Sunday * 04/23/2018, Monday * 04/26/2018, Thursday */
Assessing Dates By Months
To schedule something in terms of months, we need to assess the number of months between each occurrence, and the day of the month it will occur on.
The source code would look like this:
/// <summary> /// Gets a list of valid dates related to months within a scheduled range. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="endDate">The end date.</param> /// <param name="numberOfMonths">The number of months to skip per date.</param> /// <param name="dayOfMonth">The day of the month the event occurs.</param> /// <returns></returns> public static DateTime[] GetValidMonthsBySchedule( DateTime startDate, DateTime endDate, int numberOfMonths, int dayOfMonth) { // List to return var list = new List<DateTime>(); // Set current day as the start date with the time stripped off var currentDate = new DateTime(startDate.Year, startDate.Month, dayOfMonth); // Must have a valid start and end date, and // the end date must be later in time than the start date. if (startDate == default(DateTime) || endDate == default(DateTime) || !(endDate >= startDate)) { return list.ToArray(); } // Process // 1) Do not exceed the end date while (endDate.Subtract(currentDate).TotalDays >= 0) { // Add to list list.Add(currentDate); // Get new date currentDate = currentDate.AddMonths(numberOfMonths); } // Return return list.ToArray(); } /// <summary> /// Gets a list of valid dates related to months within a number of occurrences. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="numberOfMonths">The number of months to skip per date.</param> /// <param name="dayOfMonth">The day of the month the event occurs.</param> /// <param name="numberOfOccurrences">The number of occurrences. The default is 1.</param> /// <param name="maximumYearsAllowed">The maximum years allowed. The default is 10.</param> /// <returns></returns> public static DateTime[] GetValidMonthsByOccurrence( DateTime startDate, int numberOfMonths, int dayOfMonth, int numberOfOccurrences = 1, int maximumYearsAllowed = 10) { // List to return var list = new List<DateTime>(); // Set current day as the start date with the time stripped off var currentDate = new DateTime(startDate.Year, startDate.Month, dayOfMonth); // Must have a valid start and end date, and the occurrence count must not be exceeded. if (startDate == default(DateTime) || list.Count >= numberOfOccurrences) { return list.ToArray(); } // Process // 1) Do not exceed occurrence, and // 2) do not exceed the maximum allowed years in a date range. var maxDate = startDate.AddYears(maximumYearsAllowed); while (list.Count < numberOfOccurrences && maxDate >= currentDate) { // Add to list list.Add(currentDate); // Get new date currentDate = currentDate.AddMonths(numberOfMonths); } // Return return list.ToArray(); }
An example of how to use this in a console application:
// Main static void Main(string[] args) { var startDate = new DateTime(2018, 2, 17); var endDate = startDate.AddYears(2); // Occurs on the 5th day of every month until the end date var dates = ScheduleHelper.GetValidMonthsBySchedule(startDate, endDate, 1, 5); Console.WriteLine($"By date range ({dates.Length}):"); foreach (var date in dates) { Console.WriteLine($"{date:MM/dd/yyy, dddd}"); } // Spacer Console.WriteLine(""); // Occurs on the 5th day of every month until 20 occurrences have been reached. var dates2 = ScheduleHelper.GetValidMonthsByOccurrence(startDate, 1, 5, 20, 2); Console.WriteLine($"By # of occurrences ({dates2.Length}):"); foreach (var date in dates2) { Console.WriteLine($"{date:MM/dd/yyyy, dddd}"); } // Hold Console.Read(); } /* Result: * * By date range (25): * 02/05/2018, Monday * 03/05/2018, Monday * 04/05/2018, Thursday * 05/05/2018, Saturday * 06/05/2018, Tuesday * 07/05/2018, Thursday * 08/05/2018, Sunday * 09/05/2018, Wednesday * 10/05/2018, Friday * 11/05/2018, Monday * 12/05/2018, Wednesday * 01/05/2019, Saturday * 02/05/2019, Tuesday * 03/05/2019, Tuesday * 04/05/2019, Friday * 05/05/2019, Sunday * 06/05/2019, Wednesday * 07/05/2019, Friday * 08/05/2019, Monday * 09/05/2019, Thursday * 10/05/2019, Saturday * 11/05/2019, Tuesday * 12/05/2019, Thursday * 01/05/2020, Sunday * 02/05/2020, Wednesday * * By # of occurrences (20): * 02/05/2018, Monday * 03/05/2018, Monday * 04/05/2018, Thursday * 05/05/2018, Saturday * 06/05/2018, Tuesday * 07/05/2018, Thursday * 08/05/2018, Sunday * 09/05/2018, Wednesday * 10/05/2018, Friday * 11/05/2018, Monday * 12/05/2018, Wednesday * 01/05/2019, Saturday * 02/05/2019, Tuesday * 03/05/2019, Tuesday * 04/05/2019, Friday * 05/05/2019, Sunday * 06/05/2019, Wednesday * 07/05/2019, Friday * 08/05/2019, Monday * 09/05/2019, Thursday */
Assessing Dates By Years
Finally, to schedule something in terms of years, we need to assess the number of years between each occurrence, the specific month in that year, and the specific day in that month.
The source code would look like this:
/// <summary> /// Gets a list of valid dates related to years within a scheduled range. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="endDate">The end date.</param> /// <param name="numberOfYears">The number of years to skip per date.</param> /// <param name="month">The month the event occurs.</param> /// <param name="day">The day of the month the event occurs.</param> /// <returns></returns> public static DateTime[] GetValidYearsBySchedule( DateTime startDate, DateTime endDate, int numberOfYears, int month, int day) { // List to return var list = new List<DateTime>(); // Set current day as the start date with the time stripped off var currentDate = new DateTime(startDate.Year, month, day); // Must have a valid start and end date, and // the end date must be later in time than the start date. if (startDate == default(DateTime) || endDate == default(DateTime) || !(endDate >= startDate)) { return list.ToArray(); } // Process // 1) Do not exceed the end date, and while (endDate.Subtract(currentDate).TotalDays >= 0) { // Add to list list.Add(currentDate); // Get new date currentDate = currentDate.AddYears(numberOfYears); } // Return return list.ToArray(); } /// <summary> /// Gets a list of valid dates related to years within a scheduled range. /// </summary> /// <param name="startDate">The start date.</param> /// <param name="numberOfYears">The number of years to skip per date.</param> /// <param name="month">The month the event occurs.</param> /// <param name="day">The day of the month the event occurs.</param> /// <param name="numberOfOccurrences">The number of occurrences. The default is 1.</param> /// <param name="maximumYearsAllowed">The maximum years allowed. The default is 10.</param> /// <returns></returns> public static DateTime[] GetValidYearsByOccurrence( DateTime startDate, int numberOfYears, int month, int day, int numberOfOccurrences = 1, int maximumYearsAllowed = 10) { // List to return var list = new List<DateTime>(); // Set current day as the start date with the time stripped off var currentDate = new DateTime(startDate.Year, month, day); // Must have a valid start and end date, and // the occurrence count must not be exceeded. if ((startDate == default(DateTime)) || list.Count >= numberOfOccurrences) { return list.ToArray(); } // Process // 1) Do not exceed occurrence, and // 2) do not exceed the maximum allowed years in a date range. var maxDate = startDate.AddYears(maximumYearsAllowed); while (list.Count < numberOfOccurrences && maxDate >= currentDate) { // Add to list list.Add(currentDate); // Get new date currentDate = currentDate.AddYears(numberOfYears); } // Return return list.ToArray(); }
An example of how to use this in a console application:
// Main static void Main(string[] args) { var startDate = new DateTime(2018, 2, 17); var endDate = startDate.AddYears(5); // Occurs on the 5th day of February of every year until the end date var dates = ScheduleHelper.GetValidYearsBySchedule(startDate, endDate, 1, 2, 5); Console.WriteLine($"By date range ({dates.Length}):"); foreach (var date in dates) { Console.WriteLine($"{date:MM/dd/yyy, dddd}"); } // Spacer Console.WriteLine(""); // Occurs on the 5th day of February of every year until 50 occurrences have been reached. var dates2 = ScheduleHelper.GetValidYearsByOccurrence(startDate, 1, 2, 5, 50, 5); Console.WriteLine($"By # of occurrences ({dates2.Length}):"); foreach (var date in dates2) { Console.WriteLine($"{date:MM/dd/yyyy, dddd}"); } // Hold Console.Read(); } /* Result: * * By date range (6): * 02/05/2018, Monday * 02/05/2019, Tuesday * 02/05/2020, Wednesday * 02/05/2021, Friday * 02/05/2022, Saturday * 02/05/2023, Sunday * * By # of occurrences (6): * 02/05/2018, Monday * 02/05/2019, Tuesday * 02/05/2020, Wednesday * 02/05/2021, Friday * 02/05/2022, Saturday * 02/05/2023, Sunday */
And that’s all there really is to it. Once you see the code for yourself you can understand that it’s not nearly as complex as you might have originally thought.
I hope that helped. So, until next time – Happy Coding!