Click here to Skip to main content
15,886,137 members
Articles / Programming Languages / C#
Article

Flexible Cron And Schedule C# 2.0

Rate me:
Please Sign up or sign in to vote.
4.18/5 (9 votes)
15 May 2007CPOL3 min read 38.9K   595   34  
Iterate through a timeline using flexible predicate system with Cron Syntax in one class - Update to Previous Flexible Time Schedule

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.

C#
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:

C#
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;
....................................................................
//Overload to return  IEnumerable<DateTime>
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.

C#
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:

  1. 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.....).

    C#
    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.

  2. 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.

C#
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:

C#
return Hours().GetEnumerator 

or

C#
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.

C#
/// <summary>
/// This example says it all.
/// Iterates 2007-2008, months 1-6 , days 1-14 where is even day and not weekend
/// </summary>
[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));
    }
}
/// <summary>
///  Prints out a work hours timetable
/// </summary>
[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));
    }
}

// Demonstrating offloading Predicate and Action delegate to functions
// Can build library of Actions and or Predicates in this manor
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

License

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


Written By
Australia Australia
Interested in financial math and programming theory in general. Working on medical applications in spare time. Happy to get feedback.

Comments and Discussions

 
-- There are no messages in this forum --