ScheduleWidget

Tags: ScheduleWidget

POWERFUL, EASY, AND FREE.

ScheduleWidget is a .NET 4.5.1 scheduling engine that handles recurring events for calendars. I was influenced by Martin Fowler's white paper "Recurring Events for Calendars" in which he describes the broad software architecture for a recurring events scheduling engine. But I did not find any implementation of his idea in the wild. So this led me to write it myself. It's pretty easy to use ScheduleWidget. Suppose you have an event that occurs every Monday and Wednesday. Here's the code:

var aEvent = new Event()
{
    ID = 1,
    Title = "Every Mon and Wed",
    FrequencyTypeOptions = FrequencyTypeEnum.Weekly,    
    DaysOfWeekOptions = DayOfWeekEnum.Mon | DayOfWeekEnum.Wed
};

var schedule = new Schedule(aEvent);

That's it. You can model daily, weekly, and monthly intervals. I've written several tutorials below to introduce ScheduleWidget in more depth. Get the sample ASP.NET MVC 4 project on the github repo to see a fully-functional working copy using the FullCalendar javascript calendar control.

Tutorial 1: Modeling the Critical Mass bicycle event which occurs the last Friday of every month.

Tutorial 2: A slight twist in that "Fair Weather" Critical Mass occurs the last Friday of every month between May and September.

Tutorial 3: Exploring schedule methods like PreviousOccurrence, NextOccurrence, and IsOccurring.

ScheduleWidget and FullCalendar: Display recurring events from ScheduleWidget on the FullCalendar JQuery plugin control.

ScheduleWidget Database Persistence: Pull scheduled events from the database and initialize a schedule object.

It would be wise to read Fowler's paper to get an idea of the problem domain. If you want to use the engine it is available for free as a nuget package here: 

http://nuget.org/packages/ScheduleWidget

ScheduleWidget 2.1.0 Minor Update

  • Modified Schedule class to add Event property with a getter to the schedule's event that was passed into the constructor. This is useful for getting event properties like ID or title from the schedule itself.
  • Housekeeping: removed unnecessary System.Web assembly reference from nuget nuspec manifest.

ScheduleWidget 2.2.0 Update

  • Dajo Hein contributed recurring events for every nn weeks. To support weekly events (e.g., recur every 2 weeks on a Mon) an Event class now has the nullable DateTime property FirstDateTime.
  • Kurt Mang contributed recurring quarterly events.
  • James Still added yearly (anniversary) events.
  • Upgraded to .NET 4.5

ScheduleWidget 2.5.0 Update

  • Blake contributed breaking changes around Schedule and Event classes. 
  • Event class FirstDate property changed to StartDateTime and new property EndDateTime added
  • Event class DayInterval and WeeklyInterval deprecated for new property RepeatInterval
  • DateRange class improved around start and end date comparisons

Unit Test Examples

Below are some typical unit tests to introduce you further to the tool. Go to the source control on GitHub to see all 80+ unit tests.

[TestFixture]
public class StreetCleaningTests
{
    [Test]
    public void StreetCleaningLaborDayTest()
    {
        var holidays = GetHolidays();
        var aEvent = CreateStreetCleaningEvent();
        var schedule = new Schedule(aEvent, holidays);

        Assert.IsNotNull(holidays);
        Assert.IsNotNull(aEvent);
        Assert.IsNotNull(schedule);

        // Labor Day
        Assert.IsFalse(schedule.IsOccurring(new DateTime(2012, 9, 3)));
        Assert.IsFalse(schedule.IsOccurring(new DateTime(2013, 9, 2)));
        // Third Monday in Sep
        Assert.IsTrue(schedule.IsOccurring(new DateTime(2014, 9, 15)));
    }

    [Test]
    public void StreetCleaningIndependenceDayTest()
    {
        var holidays = GetHolidays();
        var aEvent = CreateStreetCleaningEvent();
        var schedule = new Schedule(aEvent, holidays);

        Assert.IsNotNull(holidays);
        Assert.IsNotNull(aEvent);
        Assert.IsNotNull(schedule);
        Assert.IsFalse(schedule.IsOccurring(new DateTime(2012, 7, 4)));
        Assert.IsFalse(schedule.IsOccurring(new DateTime(2016, 7, 4)));
    }

    [Test]
    public void StreetCleaningPreviousOccurrenceTest()
    {
        var holidays = GetHolidays();
        var aEvent = CreateStreetCleaningEvent();
        var schedule = new Schedule(aEvent, holidays);

        Assert.IsNotNull(holidays);
        Assert.IsNotNull(aEvent);
        Assert.IsNotNull(schedule);

        var prev = schedule.PreviousOccurrence(new DateTime(2016, 6, 6));
        Assert.IsTrue(prev.Equals(new DateTime(2016, 5, 16)));
        Assert.IsFalse(prev.Equals(new DateTime(2016, 6, 6)));
        Assert.IsFalse(prev.Equals(new DateTime(2016, 6, 20)));
    }

    [Test]
    public void StreetCleaningNextOccurrenceTest()
    {
        var holidays = GetHolidays();
        var aEvent = CreateStreetCleaningEvent();
        var schedule = new Schedule(aEvent, holidays);

        Assert.IsNotNull(holidays);
        Assert.IsNotNull(aEvent);
        Assert.IsNotNull(schedule);

        var next = schedule.NextOccurrence(new DateTime(2016, 6, 6));
        Assert.IsTrue(next.Equals(new DateTime(2016, 6, 20)));
        Assert.IsFalse(next.Equals(new DateTime(2016, 5, 16)));
        Assert.IsFalse(next.Equals(new DateTime(2016, 6, 6)));
    }


    [Test]
    public void StreetCleaningIsOccurringTest()
    {
        var holidays = GetHolidays();
        var aEvent = CreateStreetCleaningEvent();
        var schedule = new Schedule(aEvent, holidays);

        Assert.IsNotNull(holidays);
        Assert.IsNotNull(aEvent);
        Assert.IsNotNull(schedule);

        Assert.IsTrue(schedule.IsOccurring(new DateTime(2016, 6, 20)));
        Assert.IsTrue(schedule.IsOccurring(new DateTime(2016, 5, 16)));
        Assert.IsTrue(schedule.IsOccurring(new DateTime(2016, 6, 6)));

        Assert.IsFalse(schedule.IsOccurring(new DateTime(2018, 1, 1)));
        Assert.IsFalse(schedule.IsOccurring(new DateTime(2018, 2, 5)));
        Assert.IsFalse(schedule.IsOccurring(new DateTime(2018, 3, 19)));
    }

    /// <summary>
    /// Street cleaning occurs from April to October on the first and third Monday of the
    /// month, excluding holidays. In this test there are two holidays: July 4 and Labor
    /// Day (first Monday in Sep). The street cleaning example is taken directly from 
    /// Martin Fowler's white paper "Recurring Events for Calendars".
    /// </summary>
    private static Event CreateStreetCleaningEvent()
    {
        return new Event()
        {
            ID = 1,
            Title = "Fowler's Street Cleaning",
            RangeInYear = new RangeInYear()
            {
                StartMonth = 4,      // April
                EndMonth = 10,       // October
            },
            FrequencyTypeOptions = FrequencyTypeEnum.Monthly,        
            MonthlyIntervalOptions =  MonthlyIntervalEnum.First | MonthlyIntervalEnum.Third, 
            DaysOfWeekOptions = DayOfWeekEnum.Mon
        };
    }

    private static UnionTE GetHolidays()
    {
        var union = new UnionTE();
        union.Add(new FixedHolidayTE(7, 4));
        union.Add(new FloatingHolidayTE(9, DayOfWeekEnum.Mon, MonthlyIntervalEnum.First));
        return union;
    }
}