Install

PM> Install-Package ScheduleWidget.Core -Version 2.1.0

GitHub

https://github.com/jamesstill/ScheduleWidget.Core

Quick Start

ScheduleWidget is a scheduling engine that creates recurring events for calendar. It is an implementation based on Martin Fowler's white paper Recurring Events for Calendars in which he describes the software design. This is a complete .NET Core 2.0 rewrite of the popular .NET Framework version. Suppose we want to create a schedule that describes the first Monday of every month:

var builder = new ScheduleBuilder();

var schedule = builder
    .DuringMonth(WeekInterval.First)
    .OnDaysOfWeek(DayInterval.Mon)
    .HavingFrequency(FrequencyType.MonthlyByDayInMonth)
    .Create();
    

Once we create a Schedule we can ask it questions. The Schedule exposes methods (documented in more detail below) that return schedule occurrences (concrete DateTime value objects) over a defined period of time. Suppose DateTime.Today is 20 December 2030:

var during = new DateRange(DateTime.Today, DateTime.Today.AddMonths(6));

foreach (var date in schedule.Occurrences(during))
{
    Debug.WriteLine(date.ToShortDateString());
}

// Output CultureInfo("en-US"):

1/6/2031
2/3/2031
3/3/2031
4/7/2031
5/5/2031
6/2/2031
    

That's all there is to it. Read on for detailed documentation and code samples.

Documentation

Jump to...

ScheduleBuilder

When you create a schedule you must first instantiate a ScheduleBuilder. This class exposes a fluent interface to permit you to describe the schedule.

ScheduleBuilder Methods

OnDaysOfWeek(DayInterval);
DuringMonth(WeekInterval);
DuringMonthOfQuarter(MonthOfQuarterInterval);
DuringQuarter(QuarterInterval);
DuringYear(ScheduleRangeInYear);
OnAnniversary(ScheduleAnnual);
Excluding(TemporalExpressionUnion);
HavingFrequency(FrequencyType);

Notice that the HavingFrequency method takes a FrequencyType:

FrequencyType

public enum FrequencyType
{
    None = 0,
    Daily = 1,
    Weekly = 2,
    MonthlyByDayOfMonth = 3,
    MonthlyByDayInMonth = 4,
    MonthlyByDayInWeekOfMonth = 5,
    Quarterly = 6,
    Yearly = 7
}

FrequencyType Explained

FrequencyType Explanation
Daily (1) Occurs every day of every week.
Weekly (2) Occurs on selected days of every week. For example, Physics 101 occurs on Mon, Wed, and Fri of every week.
MonthlyByDayOfMonth (3) Occurs exactly one day out of each month. For example, the rent payment occurs on the 5th of every month regardless of the day of week or the week of month.
MonthlyByDayInMonth (4) Occurs on the Nth selected days of each month. For example, Physics 201 occurs on the first and third Tue and Thu of the month.
MonthlyByDayInWeekOfMonth (5) Occurs on the selected days of the Nth weeks of each month. For example, Physics 301 occurs on each Tue and Thu of the first and third weeks of the month. Note that this is a subtle distinction from FrequencyType.MonthlyByDayInMonth where the WeekInterval describes the Nth occurrence of the day within the month. Here the WeekInterval describes the Nth occurrence of the week within the month.
Quarterly (4) Occurs on selected days of selected weeks on months within a quarter and across quarters. This is the most complicated scheduling scenario. For example, an accounting audit could be the last Fri of the last month of each quarter. Or more complicated: bicycle maintenance could be the first and fourth Mon of the first month of the second and fourth quarters.
Yearly (5) Occurs on exactly one day of the year. For example, Julie's birthday is always on 15 Sep regardless of the day of week or the week of month.

Every schedule must have a FrequencyType. The default is None which means there is no schedule at all. So always call this method and set a value. You do not have to use the enumerated constant. Suppose you are rehydrating a schedule from a database. You could store the int value in the database and pass that into the builder like so: HavingFrequency(2).

The other interval types are implemented as bit fields with a set of flags that can be mixed and matched to describe the recurrence of the schedule.

DayInterval

[Flags]
public enum DayInterval
{
    None = 0,
    Sun = 1,
    Mon = 2,
    Tue = 4,
    Wed = 8,
    Thu = 16,
    Fri = 32,
    Sat = 64,
    All = Sun | Mon | Tue | Wed | Thu | Fri | Sat
}

WeekInterval

[Flags]
public enum WeekInterval
{
    None = 0,
    First = 1,
    Second = 2,
    Third = 4,
    Fourth = 8,
    Last = 16
}

MonthOfQuarterInterval

[Flags]
public enum MonthOfQuarterInterval
{
    None = 0,
    First = 1,
    Second = 2,
    Third = 4
}

QuarterInterval

[Flags]
public enum QuarterInterval
{
    None = 0,
    First = 1,
    Second = 2,
    Third = 4,
    Fourth = 8,
    All = First | Second | Third | Fourth
}

Just like with the FrequencyType you can store these values in a database. Suppose a schedule occurs every Mon, Wed, and Fri. You can store 42 in the database and rehydrate the schedule by calling:

var builder = new ScheduleBuilder();

var schedule = builder
    .OnDaysOfWeek(42)
    .HavingFrequency(2)
    .Create();

Fluent Interface Dependencies

Certain methods must be called depending upon the FrequencyType used:

Frequency Method(s) To Use Example
Daily (1) -- Every day of every week
Weekly (2) OnDaysOfWeek Every Mon, Wed, and Fri
MonthlyByDayOfMonth (3) DuringMonth The 24th of each month.
MonthlyByDayInMonth (4) DuringMonth The first and third Mon of each month.
MonthlyByDayInWeekOfMonth (5) DuringMonth Mon of the first and third week of each month.
Quarterly (6) OnDaysOfWeek
DuringMonth
DuringMonthOfQuarter
DuringQuarter
The last Fri of the last week of every quarter
Yearly (7) OnAnniversary Every Sep 15

One Time Only Occurrence

A one-time only occurrence does not need a Schedule so you can use a simple ScheduleDate instead.

One-Time Only Occurrence

// no need for a schedule
var d1 = new ScheduleDate(DateTime.Today);
Debug.Assert(d1.Includes(DateTime.Today));
Debug.Assert(!d1.Includes(DateTime.Today.AddDays(1)));
        

Schedule

Schedule Methods

public bool IsOccurring(DateTime aDate);
public IEnumerable<DateTime> Occurrences();
public IEnumerable<DateTime> Occurrences(DateRange during);
public DateTime? NextOccurrence(DateTime aDate);
public DateTime? NextOccurrence(DateTime aDate, DateRange during);
public DateTime? PreviousOccurrence(DateTime aDate);
public DateTime? PreviousOccurrence(DateTime aDate, DateRange during);

The IsOccurring method returns true if the date falls on the schedule or false if it does not. To get a list of schedule occurrences call the Occurrences method. It will return all dates over a default two-year period (one year ago to one year from today). Pass in a DateRange to get all schedule occurrences within that period. The remaining methods will return the previous or next nullable occurrence relative to the passed in date argument.

Example Daily Schedule

Every Day

var builder = new ScheduleBuilder();

var schedule = builder
    .OnDaysOfWeek(DayInterval.All)
    .HavingFrequency(FrequencyType.Daily)
    .Create();

var today = DateTime.Today;
var tomorrow = today.AddDays(1);
var forever = tomorrow.AddYears(1000);

Debug.Assert(schedule.IsOccurring(today));
Debug.Assert(schedule.IsOccurring(tomorrow));
Debug.Assert(schedule.IsOccurring(forever));
        

Example Weekly Schedule

Every Mon, Wed, and Fri

var builder = new ScheduleBuilder();

var schedule = builder
    .OnDaysOfWeek(DayInterval.Mon | DayInterval.Wed | DayInterval.Fri)
    .HavingFrequency(FrequencyType.Weekly)
    .Create();

var date1 = new DateTime(2030, 2, 4);
var date2 = new DateTime(2030, 2, 5);
var date3 = new DateTime(2030, 2, 6);
var date4 = new DateTime(2030, 2, 7);
var date5 = new DateTime(2030, 2, 8);

Debug.Assert(schedule.IsOccurring(date1));
Debug.Assert(schedule.IsOccurring(date3));
Debug.Assert(schedule.IsOccurring(date5));

Debug.Assert(!schedule.IsOccurring(date2));
Debug.Assert(!schedule.IsOccurring(date4));
        

Example Monthly Schedules

Multiple Week Intervals

var builder = new ScheduleBuilder();

// first and third Sat of every month
var schedule = builder
    .DuringMonth(WeekInterval.First | WeekInterval.Third)
    .OnDaysOfWeek(DayInterval.Sat)
    .HavingFrequency(FrequencyType.MonthlyByDayInMonth)
    .Create();
        

Multiple Day Intervals

var builder = new ScheduleBuilder();

// first Mon, Wed, and Fri of every month
var schedule = builder
    .DuringMonth(WeekInterval.First)
    .OnDaysOfWeek(DayInterval.Mon | DayInterval.Wed | DayInterval.Fri)
    .HavingFrequency(FrequencyType.MonthlyByDayInMonth)
    .Create();
        

First and Third Week Occurrence

var builder = new ScheduleBuilder();

// Mon, Wed, and Fri of the first and third week of every month
var schedule = builder
    .DuringMonth(WeekInterval.First | WeekInterval.Third)
    .OnDaysOfWeek(DayInterval.Mon | DayInterval.Wed | DayInterval.Fri)
    .HavingFrequency(FrequencyType.MonthlyByDayInWeekOfMonth)
    .Create();
        

Combine Week and Day Intervals

var builder = new ScheduleBuilder();

// first and last Mon, Wed, and Fri of every month
var schedule = builder
    .DuringMonth(WeekInterval.First | WeekInterval.Last)
    .OnDaysOfWeek(DayInterval.Mon | DayInterval.Wed | DayInterval.Fri)
    .HavingFrequency(FrequencyType.MonthlyByDayInMonth)
    .Create();
        

Persist to Database and Rehydrate Later

var builder = new ScheduleBuilder();

// first and last Mon, Wed, and Fri of every month
var schedule = builder
    .DuringMonth(WeekInterval.First | WeekInterval.Last)
    .OnDaysOfWeek(DayInterval.Mon | DayInterval.Wed | DayInterval.Fri)
    .HavingFrequency(FrequencyType.MonthlyByDayInMonth)
    .Create();

var dayIntervalValue = schedule.DayIntervalValue;
var weekIntervalValue = schedule.WeeklyIntervalValue;
var frequencyValue = schedule.FrequencyTypeValue;

// TODO: Persist these values to a database...

// rehydrate the schedule from the database
var rehydratedSchedule = builder
    .DuringMonth(weekIntervalValue)
    .OnDaysOfWeek(dayIntervalValue)
    .HavingFrequency(frequencyValue)
    .Create();
        

Example Quarterly Schedules

Quarterly schedules need up to four intervals configured: (1) days of week; (2) week(s) of month; (3) month(s) of the quarter; and (4) the quarter(s) of the year. For example suppose the Accounting Department does an audit on the first and third Monday of the first month of each quarter:

First Month of Each Quarter

var builder = new ScheduleBuilder();

var schedule = builder
    .DuringQuarter(QuarterInterval.All)
    .DuringMonthOfQuarter(MonthOfQuarterInterval.First)
    .DuringMonth(WeekInterval.First | WeekInterval.Third)
    .OnDaysOfWeek(DayInterval.Mon)
    .HavingFrequency(FrequencyType.Quarterly)
    .Create();
        

Last Friday of Last Month of First and Third Quarters

var builder = new ScheduleBuilder();

var schedule = builder
    .DuringQuarter(QuarterInterval.First | QuarterInterval.Third)
    .DuringMonthOfQuarter(MonthOfQuarterInterval.Third)
    .DuringMonth(WeekInterval.Last)
    .OnDaysOfWeek(DayInterval.Fri)
    .Create();
        

Example Yearly (Anniversary) Schedules

Ludwig Wittgenstein's Birthday

var anniversary = new ScheduleAnnual(4, 26);
var builder = new ScheduleBuilder();

var schedule = builder
    .OnAnniversary(anniversary)
    .HavingFrequency(FrequencyType.Yearly)
    .Create();
        

Excluding Holidays

Sometimes we want to exclude certain dates from our schedules. For example we want to be able to say "every day except holidays." ScheduleWidget provides two low-level objects to support holidays: ScheduleFixedHoliday and ScheduleFloatingHoliday. A fixed holiday is one that always occurs on a certain date:

Fixed Holiday

var independenceDayUS = new ScheduleFixedHoliday(7, 4);
var saintJamesDaySpain = new ScheduleFixedHoliday(7, 25);
var emperorBirthdayJapan = new ScheduleFixedHoliday(12, 23);
        

A ScheduleFloatingHoliday describes dates that float on the calendar like Labor Day in the U.S. which is always the first Monday of September. Note that floating holidays are different from moveable feasts on the Christian liturgical calendar, which are typically based on Easter Sunday.

Floating Holiday

var laborDayUS = new ScheduleFloatingHoliday(MonthOfYear.Sep, DayOfWeek.Monday, WeekInterval.First);
var bannedBookWeekStartDate = new ScheduleFloatingHoliday(MonthOfYear.Sep, DayOfWeek.Sunday, WeekInterval.Last);
        

Now we can put it all together to create a TemporalExpressionUnion that holds a collection of dates to be excluded from our schedule:

Exclude Dates From Schedule

// create the temporal union of holiday dates
var holidays = new TemporalExpressionUnion();
var independenceDayUnitedStates = new ScheduleFixedHoliday(7, 4);
var laborDayUnitedStates = new ScheduleFloatingHoliday(MonthOfYear.Sep, DayOfWeek.Monday, WeekInterval.First);
var christmasDay = new ScheduleFixedHoliday(12, 25);
var ludwigWittgensteinBirthday = new ScheduleFixedHoliday(4, 26);

holidays.Add(independenceDayUnitedStates);
holidays.Add(laborDayUnitedStates);
holidays.Add(christmasDay);
holidays.Add(ludwigWittgensteinBirthday);

var builder = new ScheduleBuilder();

// build a simple daily schedule excluding holidays
var schedule = builder
        .Excluding(holidays)
    .HavingFrequency(FrequencyType.Daily)
    .Create();
        

Example Street Cleaning

Martin Fowler, in his white paper Recurring Events for Calendars, describes a very involved schedule. "Street cleaning outside my old house [in Boston]," he writes, "occurs on the first and third Monday of the month between April and October, except for state holidays." Let's model his canonical example in ScheduleWidget. To do so we have to use the DuringYear method of the ScheduleBuilder class which accepts a ScheduleRangeInYear to describe the start and end months of the schedule within any given year.

Boston Street Cleaning Schedule

// Boston observes both U.S. Independence Day and Labor Day
var independenceDayUnitedStates = new ScheduleFixedHoliday(7, 4);
var laborDayUnitedStates = new ScheduleFloatingHoliday(MonthOfYear.Sep, DayOfWeek.Monday, WeekInterval.First);

var holidays = new TemporalExpressionUnion();
holidays.Add(independenceDayUnitedStates);
holidays.Add(laborDayUnitedStates);

var rangeInYear = new ScheduleRangeInYear(4, 10); // Apr thru Oct

var builder = new ScheduleBuilder();

var schedule = builder
    .DuringYear(rangeInYear)
    .DuringMonth(WeekInterval.First | WeekInterval.Third)
    .OnDaysOfWeek(DayInterval.Mon)
    .Excluding(holidays)
    .HavingFrequency(FrequencyType.MonthlyByDayInMonth)
    .Create();

// quick smoke test
var firstMondayInOctober = new DateTime(2040, 10, 1);
var thirdMondayInJuly = new DateTime(2040, 7, 16);

Debug.Assert(schedule.IsOccurring(firstMondayInOctober));
Debug.Assert(schedule.IsOccurring(thirdMondayInJuly));
        

Bit Values

// remember you can always pass in the bit values
var schedule = builder  
    .DuringYear(rangeInYear)
    .DuringMonth(5) // 1st and 3rd weeks of month
    .OnDaysOfWeek(2) // Mon
    .Excluding(holidays)
    .HavingFrequency(4) // monthly by day in month
    .Create();
        

Easter

In 725 CE, Saint Bede first defined Easter as "the Sunday following the full Moon which falls on or after the [Spring] equinox." The astronomical equinox can fall either on 19, 20, or 21 March. The ecclesiastical date simplifies things by fixing the equinox to 21 March.

Easter (Western)

// Easter for the year 2029
var aDate = new ScheduleDate(Easter.GetEasterSunday(2029));
        

Easter (Orthodox)

// Easter for the year 2029
var aDate = new ScheduleDate(Easter.GetEasterSunday(2029, EasterCalendar.Orthodox));
        

Support

Need help? Ctrl-F on the documentation above. I probably covered it there. If not post your question with a schedulewidget tag to StackOverflow schedulewidget tagged questions section. I'll try to answer any questions there as quickly as possible.

Found a bug? You can e-mail me directly at james AT squarewidget DOT com. The chance of my getting it fixed quickly is in direct correlation to the amount of detail you can send me (steps to repro, current behavior, desired behavior, and so on). Pull requests most welcome! Please study the code first and make sure your fix is in line with the software design.