Introduction
This article is about flexible compact design for a class that outputs all dates in the range using Unix Cron syntax. The design facilitates flexibility via attachment of predicates using where
statements (similar to LINQ) anywhere in the call chain.
See previous article for background information.
Background
Predicates and Actions (Predicate<T>
, Action<T>
) are a couple of the versatile lightweight generics in C# 2.0.
public delegate bool Predicate<T>(T obj);
public delegate <a>void</a> Action<T>(T obj);
Predicates are used within Orcas for construction of the Where
statements included in LINQ syntax statements such as:
Predicate<DateTime> pred = new Predicate<DateTime>(delegate(DateTime date)
{
return date.DayOfWeek == DayOfWeek.Monday;
});
var dates =
from dte in GetEnumerator(new DateTime(2007, 1, 1),
DateTime.Today, 0, 0, 1)
where pred(dte)
select dte;
....................................................................
private IEnumerable<DateTime> GetEnumerator(DateTime start, DateTime end,
int stepyear, int stepmonth, int stepdays)
{
while (start <= end)
{
if (stepyear > 0)
{
start = start.AddYears(stepyear);
}
if (stepmonth > 0)
{
start = start.AddMonths(stepmonth);
}
if (stepdays > 0)
{
start = start.AddDays(stepdays);
}
yield return start;
}
}
When declared inline such as below, the compiler creates the predicate for you.
var dates =
from dte in GetEnumerator(new DateTime(2007, 1, 1),
DateTime.Today, 0, 0, 1)
where dte.DayOfWeek == DayOfWeek.Monday
select dte;
In both instances, a predicate is created that holds the anonymous method that is then applied to filter the output.
When designing code that iterates over multiple items, it (generally anything that implements IEnumerable
) has design benefits from the use of the predicate.
Explanation of the Classes
Since LINQ is not mainstream yet, I would like to present two classes:
Schedule
Uses a predicated iterator pattern to iterate over dates. Put simply, when you run foreach
on a series of dates,only dates not filtered out by the applied predicate will yield.
It also uses a recursive iterator syntax for iterating over multiple hierarchical schedules(Years, months.....).
foreach (DateTime date in schedule4.GetEnumerator
(schedule3.GetEnumerator(schedule2.GetEnumerator(schedule1))))
{
Console.WriteLine(date.ToString("f"));
}
The schedule is meant to be flexible. Allowing iterators to 'chain' adds some degree of optimisation. In this way, one schedule iterates through months and another subordinate iterates those months outputting yielded Days of the month and so on down the chain.
See previous article for background information.
Cron
This is really my second attempt and an experiment in design. The Cron
class is designed to allow iteration over dates without unnecessary spin. By that, I mean no wasted foreach cycles waiting for a predicate to yield. (I personally like the predicated iterator pattern and believe it sufficient for almost all use cases). It is also an attempt to simulate Unix Cron syntax (as far as I understand that syntax - Cron.Year(1900).Month(1).Day(1,15)
). It is also a radical departure from the Schedule
iterator pattern.
Interesting Use of Iteration
Basically we modify the return of GetEnumerator
to reflect whether we are enumerating
years, hours, etc. i.e. state dependent iteration return.
Only the final GetEnumerator
in the call chain is called by the user (implicitly by the foreach
call), thus dramatically simplifying syntax. That call is passed up a chain of Cron
created by the calling syntax chain itself thus enabling chaining.
foreach (DateTime date in
Cron.Years(2007).Month(1).Day(1).Hour(1, 2).Minute(20, 25))
{ .........
Of interest is the use of Functions returning IEnumerable<DateTime>
within the class that is the enabler in this pattern. (These are not named / GOF type patterns).
In traditional style coding, the return on GetEnumerator
is either an IEnumerator
or yield construct. Here, GetEnumerator
returns an IEnumerator
that is derived from calling GetEnumerator
on the return of one of several Time
functions. For example:
return Hours().GetEnumerator
or
return Minutes().GetEnumerator
Cron
maintains flexible efficient design.
Cron
is also efficient in the way it iterates dates.
It outputs all dates in the range using Unix Cron syntax.
Cron
calling chain syntax allows the attachment of Predicates
using Where
functions (similar to LINQ) anywhere in the call chain.
Moreover, Actions
can be attached in the call chain.
[TestAttribute]
public void Y07_08_M01_12_D1_31()
{
foreach (DateTime date in Cron.Years(2007, 2008).Action(ActionYear).Month(1, 6).
Action(ActionMonth).Day(1, 14).Action(ActionDay).
Where(BusinessDay).Where(EvenDay))
{
Assert.GreaterOrEqual(date.Year, 2007);
Assert.LessOrEqual(date.Year, 2008);
Assert.GreaterOrEqual(date.Month, 1);
Assert.LessOrEqual(date.Month, 6);
Assert.GreaterOrEqual(date.Day, 1);
Assert.LessOrEqual(date.Day, 14);
Assert.IsTrue(EvenDay(date));
Assert.IsTrue(BusinessDay(date));
}
}
[TestAttribute]
public void WORKTimeTable()
{
foreach (DateTime date in Cron.Years(2007, 2008).Action(ActionYear).Month(1, 6).
Action(ActionMonth).Day(1, 14).Action(ActionDay).Where(BusinessDay).
Where(EvenDay).Hour(9,17).Action(ActionHour ))
{
Assert.GreaterOrEqual(date.Year, 2007);
Assert.LessOrEqual(date.Year, 2008);
Assert.GreaterOrEqual(date.Month, 1);
Assert.LessOrEqual(date.Month, 6);
Assert.GreaterOrEqual(date.Day, 1);
Assert.LessOrEqual(date.Day, 14);
Assert.IsTrue(EvenDay(date));
Assert.IsTrue(BusinessDay(date));
}
}
private void ActionYear(DateTime date)
{
Console.WriteLine("YEAR {0}", date.Year);
}
private void ActionMonth(DateTime date)
{
Console.WriteLine("\t\tMONTH {0:MMMM}", date );
}
private void ActionDay(DateTime date)
{
Console.WriteLine("\t\t\tDAY {0} {1}", date.DayOfWeek, date.Day );
}
private void ActionHour(DateTime date)
{
Console.WriteLine("{0:h:m tt}\t----", date);
}
private bool BusinessDay(DateTime day)
{
return day.DayOfWeek!= DayOfWeek.Saturday && day.DayOfWeek!= DayOfWeek.Sunday ;
}
private bool EvenDay(DateTime day)
{
return day.Day % 2 == 0;
}
All major classes are documented in the code.
Points of Interest
One mention I might make is that nesting multiple iterators and using 'yield break' caused a few problems during testing that I never quite resolved.
Debugging nested iterator patterns which include anonymous method delegates is likely to bring on vertigo.
History
- 15th May, 2007: Initial post
Interested in financial math and programming theory in general. Working on medical applications in spare time. Happy to get feedback.