PM> Install-Package ScheduleWidget.Core -Version 2.1.0
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.
Live Example
in the menu above to exercise a real-world example.
Jump to...
When you create a schedule you must first instantiate a ScheduleBuilder
. This class exposes a fluent interface
to permit you to describe the schedule.
OnDaysOfWeek(DayInterval); DuringMonth(WeekInterval); DuringMonthOfQuarter(MonthOfQuarterInterval); DuringQuarter(QuarterInterval); DuringYear(ScheduleRangeInYear); OnAnniversary(ScheduleAnnual); Excluding(TemporalExpressionUnion); HavingFrequency(FrequencyType);
Notice that the HavingFrequency
method takes a FrequencyType
:
public enum FrequencyType { None = 0, Daily = 1, Weekly = 2, MonthlyByDayOfMonth = 3, MonthlyByDayInMonth = 4, MonthlyByDayInWeekOfMonth = 5, Quarterly = 6, Yearly = 7 }
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.
[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 }
[Flags] public enum WeekInterval { None = 0, First = 1, Second = 2, Third = 4, Fourth = 8, Last = 16 }
[Flags] public enum MonthOfQuarterInterval { None = 0, First = 1, Second = 2, Third = 4 }
[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();
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
|
The last Fri of the last week of every quarter |
Yearly (7) |
OnAnniversary
|
Every Sep 15 |
A one-time only occurrence does not need a Schedule
so you can use a simple ScheduleDate
instead.
// 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)));
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.
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));
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));
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();
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();
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();
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 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();
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:
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();
var builder = new ScheduleBuilder(); var schedule = builder .DuringQuarter(QuarterInterval.First | QuarterInterval.Third) .DuringMonthOfQuarter(MonthOfQuarterInterval.Third) .DuringMonth(WeekInterval.Last) .OnDaysOfWeek(DayInterval.Fri) .Create();
var anniversary = new ScheduleAnnual(4, 26); var builder = new ScheduleBuilder(); var schedule = builder .OnAnniversary(anniversary) .HavingFrequency(FrequencyType.Yearly) .Create();
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:
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.
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:
// 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();
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 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));
// 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();
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 for the year 2029 var aDate = new ScheduleDate(Easter.GetEasterSunday(2029));
// Easter for the year 2029 var aDate = new ScheduleDate(Easter.GetEasterSunday(2029, EasterCalendar.Orthodox));
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.