Click here to Skip to main content
Click here to Skip to main content

Scheduling Future Dates

By , 1 Oct 2008
Rate this:
Please Sign up or sign in to vote.
Scheduler

Introduction

A few months back, I had a need to develop a service that performed a specific task on a regular basis. Not being content with a merely fulfilling the requirements, I decided that a more comprehensive (and infinitely more re-usable) solution was called for. This article discusses that solution.

The Problem

At it's face, the problem was a simple one - implement a method for predicting the date/time of a scheduled event. However, once I started defining the possible variations, it became apparent that more work would be required than initially planned. Some of the questions that presented themselves were:

  1. What intervals do I want to support?
  2. In the case of schedules that involved periods of days, weeks, or months, what time of day should an event be triggered?

The Solution

First, I made the decision to ignore seconds and milliseconds because when you're dealing with something that's on a repetitive schedule over a long period of time, you're usually dealing with rather loose timing constraints - one second one way or the other isn't going to hurt anything. Besides, you need to avoid bogging down the CPU with needless time checks every 5 milliseconds, so I think it's an acceptable trade-off. Given the open nature of the code, feel free to change this aspect if you desire.

Next, I had to come up with some usable "modes", so I decided on the following:

  • Hourly
  • Daily
  • Weekly
  • First day of the month
  • Last day of the month
  • Specific day of the month
  • Specific time interval

I know, some of them seem redundant, but hey use what you need, and discard the rest - grin.

Then, I decided that I would accept a date from which to determine a future date, based on the specified mode. At that point I came up with the prototype for the required method. Finally, I created a class that contains nothing but static methods. This frees us of the requirement to create an instance of the object before using its methods. All of the methods are public, but there are two that are especially helpful.

public static DateTime CalculateNextTriggerTime(DateTime currentTime, 
						     TimeSpan processTime, 
						     ScheduleMode scheduleMode)

This method accepts the date from which we will be calculating the future date, a TimeSpan object that is used in various ways depending on which mode you use, and the Schedule mode itself. This is where the explanation of how it works gets a little tricky. By default, if you pass in a TimeSpan that's zeroed out (new TimeSpan(0,0,0,0,0)), the processTime will have no affect on the caluclated future time. With the exception of the SpecificInterval mode, the time of day for all future dates will be set to the top of the hour for the Hourly mode, and midnight for the remaining modes. The processTime value is used to modify those calculated future dates. Once again, the seconds and milliseconds properties are ignored. Each case in the switch statement is shown below, and I've decided that the code is commented well enough to not require additional descriptive text.

ScheduleMode.Hourly

The processTime.Minutes property is added to the calculated future date (remember, it defaults to the top of the hour). This allows you to specify an hourly schedule that happens at irregular times (like at hour:15, or hour:30).

	case ScheduleMode.Hourly:
	{
		// if the time is earlier than the specified minutes
		if (nextTime.Minute < processTime.Minutes)
		{
			// simply add the difference
			nextTime = nextTime.AddMinutes(
                               processTime.Minutes - nextTime.Minute);
		}
		else
		{
			// subtract the minutes, seconds, and milliseconds - this allows 
			// us to determine what the last hour was
			nextTime = nextTime.Subtract(new TimeSpan(0, 0, 
								nextTime.Minute, 
								nextTime.Second, 
								nextTime.Millisecond));

			// add an hour and the number of minutes 
			nextTime = nextTime.Add(new TimeSpan(0, 1, 
								processTime.Minutes,
                                                                         0, 0));
		}
	}
	break;

ScheduleMode.Daily

The processTime.Hours and .Minutes properties are used to specify the time of the day at which you want to calculate. The .Days property is ignored.

	case ScheduleMode.Daily:
	{
		// subtract the hour, minutes, seconds, and milliseconds 
                  // (essentially, makes it midnight of the current day)
		nextTime = nextTime.Subtract(new TimeSpan(0, 
							nextTime.Hour,
							nextTime.Minute,
							nextTime.Second,
							nextTime.Millisecond));

		// add a day, and the number of hours:minutes after midnight
		nextTime = nextTime.Add(new TimeSpan(1, processTime.Hours, 
							processTime.Minutes, 0, 0));
	}
	break;

ScheduleMode.Weekly

The processTime.Days property specifies the numeric day of the week on which to calculate, while the .Hours and .Minutes properties are used to specify the time of the day.

	case ScheduleMode.Weekly:
	{
		int daysToAdd = 0;
		// get the number of the week day

		int dayNumber = (int)nextTime.DayOfWeek;
		// if the process day isn't today (should only happen when the
                  // service starts)
		if (dayNumber == processTime.Days)
		{
			// add 7 days
			daysToAdd = 7;
		}
		else
		{
			// determine where in the week we are
			// if the day number is greater than than the specified day
			if (dayNumber > processTime.Days)
			{
				// subtract the day number from 7 to get the number
                                     // of days to add
				daysToAdd = 7 - dayNumber;
			}
			else
			{
				// otherwise, subtract the day number from the
                                     // specified day
				daysToAdd = processTime.Days - dayNumber;
			}
		}
		// add the days
		nextTime = nextTime.AddDays(daysToAdd);

		// get rid of the seconds/milliseconds
		nextTime = nextTime.Subtract(new TimeSpan(0, nextTime.Hour, 
								nextTime.Minute, 
								nextTime.Second, 
								nextTime.Millisecond));

		// add the specified time of day
		nextTime = nextTime.Add(new TimeSpan(0, processTime.Hours, 
							processTime.Minutes, 0, 0));
	}
	break;

ScheduleMode.FirstDayOfMonth

The processTime.Hours and .Minutes properties are used to specify the time of the day. The .Days property is ignored.

	case ScheduleMode.FirstDayOfMonth:
	{
		// detrmine how many days in the month
		int daysThisMonth = DaysInMonth(nextTime);

		// for ease of typing
		int today = nextTime.Day;

		// if today is the first day of the month
		if (today == 1)
		{
			// simply add the number of days in the month
			nextTime = nextTime.AddDays(daysThisMonth);
		}
		else
		{
			// otherwise, add the remaining days in the month
			nextTime = nextTime.AddDays((daysThisMonth - today) + 1);
		}

		// get rid of the seconds/milliseconds
		nextTime = nextTime.Subtract(new TimeSpan(0,
							nextTime.Hour,
							nextTime.Minute,
							nextTime.Second,
							nextTime.Millisecond));

		// add the specified time of day
		nextTime = nextTime.Add(new TimeSpan(0, processTime.Hours, 
							processTime.Minutes, 0, 0));
	}
	break;

ScheduleMode.LastDayOfMonth

The processTime.Hours and .Minutes properties are used to specify the time of the day. The .Days property is ignored.

	case ScheduleMode.LastDayOfMonth:
	{
		// determine how many days in the month
		int daysThisMonth = DaysInMonth(nextTime);

		// for ease of typing
		int today = nextTime.Day;

		// if this is the last day of the month
		if (today == daysThisMonth)
		{
			// add the number of days for the next month
			int daysNextMonth = DaysInMonth(nextTime.AddDays(1));
			nextTime = nextTime.AddDays(daysNextMonth);
		}
		else
		{
			// otherwise, add the remaining days for this month
			nextTime = nextTime.AddDays(daysThisMonth - today);
		}

		// get rid of the seconds/milliseconds
		nextTime = nextTime.Subtract(new TimeSpan(0, 
							nextTime.Hour, 
							nextTime.Minute, 
							nextTime.Second, 
							nextTime.Millisecond));

		// add the specified time of day
		nextTime = nextTime.Add(new TimeSpan(0, processTime.Hours, 
							processTime.Minutes, 0, 0));
	}
	break;

ScheduleMode.DayOfMonth

The processTime.Hours and .Minutes properties are used to specify the time of the day. The .Days property is ignored.

	case ScheduleMode.DayOfMonth:
	{
		// account for leap year
		// assume we don't have a leap day 
		int leapDay = 0;

		// if it's february, and a leap year and the day is 29
		if (nextTime.Month == 2 && !IsLeapYear(nextTime) &&
                      processTime.Days == 29)
		{
			// we have a leap day
			leapDay = 1;
		}

		int daysToAdd = 0;
		// if the current day is earlier than the desired day
		if (nextTime.Day < processTime.Days)
		{
			// add the difference (less the leap day)
			daysToAdd = processTime.Days - nextTime.Day - leapDay;
		}
		else
		{
			// otherwise, add the days not yet consumed (less the leap day)
			daysToAdd = (DaysInMonth(nextTime) - nextTime.Day) +
                              processTime.Days - leapDay;
		}

		// add the calculated days
		nextTime = nextTime.AddDays(daysToAdd);

		// get rid of the seconds/milliseconds
		nextTime = nextTime.Subtract(new TimeSpan(0, 
							nextTime.Hour, 
							nextTime.Minute, 
							nextTime.Second, 
							nextTime.Millisecond));

		// add the specified time of day
		nextTime = nextTime.Add(new TimeSpan(0, processTime.Hours, 
							processTime.Minutes, 0, 0));
	}
	break;

ScheduleMode.SpecificInterval

The processTime object is the entire basis for calculated the future date.

	case ScheduleMode.SpecificInterval:
	{
		// if we're past the 30-second mark, add a minute to the current time
		if (nextTime.Second >= 30)
		{
			nextTime = nextTime.AddSeconds(60 - nextTime.Second);
		}

		// since we don't care about seconds or milliseconds, zero these items out
		nextTime = nextTime.Subtract(new TimeSpan(0, 0, 0, 
							nextTime.Second, 
							nextTime.Millisecond));

		// now, we add the process time
		nextTime = nextTime.Add(processTime);
	}
	break;

Finally, before returning the newly calculated future date, the seconds and milliseconds are once again zeroed out, just to make sure the date/time is pure.

	// and subtract the seconds and milliseconds, just in case they were 
	// specified in the process time 
	nextTime = nextTime.Subtract(new TimeSpan(0, 0, 0, 
							nextTime.Second, 
							nextTime.Millisecond));

The next most useful function is how we determine equality. I was really kind of surprised (and even dismayed) at how unreasonably difficult it is to determine date equality. First, I simply tried comparing two DateTime objects, but if I ever got an "equal" state, it was by mere chance. Next, I tried comparing the Ticks property from the two DateTimes. Again, any "equal" state that resulted was just dumb luck. After trying to figure out what the hell was wrong - it struck me.

The internal representation of a DateTime is a floating point number. That's when I came up with my ultimate solution - convert each date to a 64-bit integer and compare the integers. Here are the methods:

public static Int64 ConvertToInt64(DateTime date)
{
	string dateStr = date.ToString("yyyyMMddHHmm");
	Int64 dateValue = Convert.ToInt64(dateStr);
	return dateValue;
}


public static DateCompareState CompareDates(DateTime now, DateTime target)
{
	DateCompareState state = DateCompareState.Equal;

	now = now.Subtract(new TimeSpan(0, 0, 0, now.Second, now.Millisecond));
	target = target.Subtract(new TimeSpan(0, 0, 0, target.Second, target.Millisecond));

	Int64 nowValue = ConvertToInt64(now);
	Int64 targetValue = ConvertToInt64(target);

	if (nowValue < targetValue)
	{
		state = DateCompareState.Earlier;
	}
	else
	{
		if (nowValue > targetValue)
		{
			state = DateCompareState.Later;
		}
	}

	return state;
}

I used the DateTime.ToString() method to create a custom string representation of the date, converted the string to a Int64, and came up with numbers that could be reliably compared. Remember, we don't have to worry about seconds or milliseconds in this class, so a Int64 will always be big enough. If a need ever arises to require seconds and milliseconds, I could always switch to a decimal type.

The Sample Application

The sample application has two listboxes sitting side-by-side. The left-most one allows you to fire off a thread that calculates a future date 2 minutes in the future. You have to click the Start button to make it go, and it keeps going until you close the application. The right-most listbox displays examples of using all of the scheduling modes, and is populated immediately upon staring the application.

A BackgroundWorker object is used to continuously schedule an event, and is a perfect example of how you probably want to implement it in your own code. Here's the DoWork function (since it's fully commented, I won't bother with additional text about it in this article):

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
	// cast the sender to a BackgroundWorker object so we can access its properties
	BackgroundWorker worker = sender as BackgroundWorker;

	// set the current date/time
	DateTime nextTime = DateTime.Now;

	// set the interval - I do this because I see no value at all in re-allocating 
	// memory over and over again if it's not needed. For this demo, we use two 
	// minutes so we're not waiting for the next millenia to occur.
	TimeSpan interval = new TimeSpan(0, 0, m_triggerMinutes, 0, 0);

	// set our mode here to relieve typing elsewhere
	ScheduleMode mode = ScheduleMode.SpecificInterval;

	// we need to know when the comparison state changes
	DateCompareState compareState;

	// these variables allow us to remain responsive to the UI, and reduce load 
	// on the CPU by checking for a date/time match every second. If this code 
	// was being run in a service, you could probably afford to set sleepTime to 
	// 1000 which would further reduce CPU load.
	int tick = 0;
	int sleepTime = 250;
	int checkAt = 1000;

	// process until the thread has been cancelled
	while (!worker.CancellationPending)
	{

		// calculate our next trigger time
		nextTime = DateScheduler.CalculateNextTriggerTime(nextTime, interval,
                      mode);
		tick = 0;

		// check the time until the thread ghas been cancelled
		while (!worker.CancellationPending)
		{

			// if it's time to compare the times
			if (tick % checkAt == 0)
			{

				// compare the time
				compareState =  DateScheduler.CompareDates(
                                        DateTime.Now, nextTime);

				// set our tick monitor to 0
				tick = 0;

				// if the dates are equal, break out of this while loop
				if (compareState == DateCompareState.Equal)
				{
					break;
				}
			}

			// sleep
			Thread.Sleep(sleepTime);

			// and keep track of how long we slept
			tick += sleepTime;
		}

		// Perform your scheduled task (for this demo, we simply update the 
		// list box in the form)
		if (!worker.CancellationPending)
		{
			worker.ReportProgress(0);
		}
	}
}

As you can see, the source code is fairly well documented and there's several examples of using the class, so go forth and schedule to your hearts content.

History

10/01/2008: Original article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

John Simmons / outlaw programmer
Software Developer (Senior)
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.
 
My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
GeneralWell written article [modified] PinmvpJ4amieC7-Oct-08 2:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140415.2 | Last Updated 1 Oct 2008
Article Copyright 2008 by John Simmons / outlaw programmer
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid