CFInterface For Generic Date Iteration Strategies In ColdFusion
I love using dates in ColdFusion. I love performing mathematical operations on them; I love looping over them; I love everything about them; they just seem to work so gracefully. Lately, I've been thinking about updating my Kinky Calendar ColdFusion calendar application and one of the things I wanted to do was evolve the way event repetition gets calculated. As a first step down this path, I wanted to fool around with creating a date iteration interface.
While I don't really need to use this, I thought it would be fun to actually use ColdFusion 8's new CFInterface tag to define the method signatures of all date iterating objects. For this interface, I borrowed heavily from the Java Iterator interface (minus the Remove() method):
<cfinterface
hint="I define the interface for all date iterators.">
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I return an intialized iterator">
<!--- Define arguments. --->
<cfargument
name="StartDate"
type="numeric"
required="true"
hint="I am the start date (inclusive) of the iteration. Accepts standard and numeric dates."
/>
<cfargument
name="EndDate"
type="numeric"
required="true"
hint="I am the end date (inclusive) of the iteration. Accepts standard and numeric dates."
/>
</cffunction>
<cffunction
name="HasNext"
access="public"
returntype="boolean"
output="false"
hint="I determine if there is another date over which to iterate."
/>
<cffunction
name="Next"
access="public"
returntype="date"
output="false"
hint="I return the next date (or VOID if there is no next date)."
/>
</cfinterface>
As you can see, the CFInterface defined here is quite simple. The Init() method requires a start and end date to define the inclusive date span of looping. Then, the HasNext() and Next() methods allow for conditional looping and value retrieval respectively.
Once I had the interface, I was ready to start creating concrete iteration strategies. But, based on my past experience with dates, I realized that all date iteration strategies are like 90% similar; most of the functionality - the logic that doesn't change from strategy to strategy - can be factored out. After playing around with different versions of a base class for date iteration, I finally came up with something that required only one method to be overridden by all extending classes:
<cfcomponent
displayname="BaseDateIterator"
implements="DateIterator"
hint="I provide common functionlaity for every date iterator.">
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I return an initialized date iterator.">
<!--- Define arguments. --->
<cfargument
name="StartDate"
type="numeric"
required="true"
hint="I am the start date (inclusive) of the iteration. Accepts standard and numeric dates."
/>
<cfargument
name="EndDate"
type="numeric"
required="true"
hint="I am the end date (inclusive) of the iteration. Accepts standard and numeric dates."
/>
<!--- Store the arguments. --->
<cfset VARIABLES.StartDate = Fix( ARGUMENTS.StartDate ) />
<cfset VARIABLES.EndDate = Fix( ARGUMENTS.EndDate ) />
<!---
Default the current date index to our start date.
This will be used to calculate our next date.
--->
<cfset VARIABLES.DateIndex = VARIABLES.StartDate />
<!---
The next date index will be set later in the
SetNextDate() method calls.
--->
<cfset VARIABLES.NextDateIndex = "" />
<!--- Set the next date. --->
<cfset VARIABLES.SetNextDate() />
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="HasNext"
access="public"
returntype="boolean"
output="false"
hint="I determine if there is another date over which to iterate.">
<!---
Check to see if our next date is within the bounds
of our inclusive date span.
--->
<cfreturn (
(VARIABLES.StartDate LTE VARIABLES.NextDateIndex) AND
(VARIABLES.EndDate GTE VARIABLES.NextDateIndex)
) />
</cffunction>
<cffunction
name="SetNextDate"
access="private"
returntype="void"
output="false"
hint="I determine what the next valid date of iteration is.">
<!---
Check to see if we have a next date index yet. If
not, then this is our intialization. If so, then we
are already in the midst of the iteration.
--->
<cfif NOT IsNumericDate( VARIABLES.NextDateIndex )>
<!--- We are initializing our next date. --->
<cfset VARIABLES.NextDateIndex = VARIABLES.DateIndex />
<cfelse>
<!---
The base iterator will simply iterate to the next
day, regardless of logic.
--->
<cfset VARIABLES.NextDateIndex = (VARIABLES.NextDateIndex + 1) />
</cfif>
<!--- Return out. --->
<cfreturn />
</cffunction>
<cffunction
name="Next"
access="public"
returntype="date"
output="false"
hint="I return the next date (or VOID if there is no next date).">
<!--- Check to see if we have a next. --->
<cfif THIS.HasNext()>
<!---
Since we have a next date, we need to move to
it. Let's store the next date in the current
date index.
--->
<cfset VARIABLES.DateIndex = VARIABLES.NextDateIndex />
<cfelse>
<!--- Since we have no next date, return VOID. --->
<cfreturn />
</cfif>
<!---
ASSERT: At this point, we already know the date we
are going to return. Before we do that, however, we
need to figure out if we are going to have a next
date to return after that.
--->
<!--- Set the next date. --->
<cfset VARIABLES.SetNextDate() />
<!---
Return the current date index. Since our dates are
likely stored internally in numeric, convert the
number to a full date/time format.
--->
<cfreturn CreateDateTime(
Year( VARIABLES.DateIndex ),
Month( VARIABLES.DateIndex ),
Day( VARIABLES.DateIndex ),
Hour( VARIABLES.DateIndex ),
Minute( VARIABLES.DateIndex ),
Second( VARIABLES.DateIndex )
) />
</cffunction>
</cfcomponent>
The way I have this base class set up, the only method that needs to be overridden by extending classes is the SetNextDate() method. All other pieces of functionality are generic enough to be covered in the base class. Notice also that this class implements my CFInterface tag (effectively signing a contract that it will always have the appropriate methods available).
Once I had my CFInterface defined and my base class functionality coded, it was just a matter of creating the extending classes and overriding the SetNextDate() logic. The first one I created was the EveryDay.cfc date iterator. But, because the base date iterator performs this functionality, I didn't actually have to override any methods:
<cfcomponent
displayname="EveryDay"
implements="DateIterator"
extends="BaseDateIterator"
hint="I iterate over every day between two dates.">
<!---
The base iterator does an every day iteration by
default. No need to override anything here.
--->
</cfcomponent>
Notice that this class (as well as all extending classes in this example) implements the CFInterface for DateIterator and extends the base class. Because these concrete class are all extending the base class, I believe that the Implements attribute of the CFComponent tag here is actually redundant, but I threw it in anyway.
Next, I created an iterator that hits every Monday, Wednesday, and Friday between the two given dates:
<cfcomponent
displayname="MWF"
implements="DateIterator"
extends="BaseDateIterator"
hint="I iterate over every Monday, Wednesday, and Friday between two dates.">
<cffunction
name="SetNextDate"
access="private"
returntype="void"
output="false"
hint="I determine what the next valid date of iteration is.">
<!---
Check to see if we have a next date index yet. If
not, then this is our intialization. If so, then we
are already in the midst of the iteration.
--->
<cfif NOT IsNumericDate( VARIABLES.NextDateIndex )>
<!--- We are initializing our next date. --->
<cfset VARIABLES.NextDateIndex = VARIABLES.DateIndex />
<cfelse>
<!---
We just iterated. To make sure we don't keep
iterating over the current date, add one day
to the next day.
--->
<cfset VARIABLES.NextDateIndex++ />
</cfif>
<!--- Keep looping until we are on a valid date. --->
<cfloop condition="true">
<!---
Check to see if the suggested next date is valid
for our next date index (Monday, Wednesday, Friday).
--->
<cfif ListFind( "2,4,6", DayOfWeek( VARIABLES.NextDateIndex ) )>
<!--- Return out of the loop (and function). --->
<cfreturn />
</cfif>
<!---
If we have made it this far, we are still
looking for a valid date. Add one day to the
next suggested date.
--->
<cfset VARIABLES.NextDateIndex++ />
</cfloop>
</cffunction>
</cfcomponent>
This one's a little bit more involved, but again, we are only overriding the one method that defines the next date logic.
As a final test, I created a somewhat more complicated iterator that would iterate over the second Tuesday of each month between the given dates:
<cfcomponent
displayname="SecondTuesday"
implements="DateIterator"
extends="BaseDateIterator"
hint="I iterate over every second Tuesday of the month between two dates.">
<cffunction
name="GetSecondTuesday"
access="private"
returntype="date"
output="false"
hint="I get the second tuesday of the month (for the given date).">
<!--- Define arguments. --->
<cfargument
name="Date"
type="numeric"
required="true"
hint="I am the date in the month for which we are getting the second Tuesday."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Get the first day in this month. --->
<cfset LOCAL.Month = CreateDate(
Year( ARGUMENTS.Date ),
Month( ARGUMENTS.Date ),
1
) />
<!---
Get the second tuesday based on the first date of
the month.
--->
<cfif (DayOfWeek( LOCAL.Month ) GT 3)>
<!--- Get second tuesday. --->
<cfreturn (LOCAL.Month + (8 - DayOfWeek( LOCAL.Month )) + 9) />
<cfelse>
<!--- Return second tuesday. --->
<cfreturn (LOCAL.Month + (3 - DayOfWeek( LOCAL.Month )) + 7) />
</cfif>
</cffunction>
<cffunction
name="SetNextDate"
access="private"
returntype="void"
output="false"
hint="I determine what the next valid date of iteration is.">
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!---
Check to see if we have a next date index yet. If
not, then this is our intialization. If so, then we
are already in the midst of the iteration.
--->
<cfif NOT IsNumericDate( VARIABLES.NextDateIndex )>
<!--- We are initializing our next date. --->
<!---
Get the second tuesday of the starting month.
This may not be the Tuesday that we need, but
it gives us a basis for iteration.
--->
<cfset LOCAL.Tuesday = VARIABLES.GetSecondTuesday(
VARIABLES.DateIndex
) />
<!---
Check to see if this first tuesday is before our
start date. If so, then we need to start in the
next month.
--->
<cfif (VARIABLES.DateIndex LTE LOCAL.Tuesday)>
<!--- Use this tuesday. --->
<cfset VARIABLES.NextDateIndex = LOCAL.Tuesday />
<cfelse>
<!--- Use the tuesday of the next month. --->
<cfset VARIABLES.NextDateIndex = VARIABLES.GetSecondTuesday(
DateAdd( "m", 1, LOCAL.Tuesday )
) />
</cfif>
<cfelse>
<!---
We just iterated. Get the second tuesday of
the next month.
--->
<cfset VARIABLES.NextDateIndex = VARIABLES.GetSecondTuesday(
DateAdd( "m", 1, VARIABLES.NextDateIndex )
) />
</cfif>
<!--- Return out. --->
<cfreturn />
</cffunction>
</cfcomponent>
The logic here is more complicated and I ended up created a secondary, private method, GetSecondTuesday(), that assists the logic required in the SetNextDate() method.
Once I had these three iteration strategies coded, I created a simple test page to see that they were working properly:
<!--- Test Every day strategy. --->
<!--- Create the date iterator. --->
<cfset objIterator = CreateObject(
"component",
"cfc.EveryDay"
).Init(
StartDate = "1/1/2009",
EndDate = "1/15/2009"
)
/>
<!--- Output dates. --->
<p>
<strong>EveryDay</strong>
</p>
<p>
<!--- Loop over dates. --->
<cfloop condition="objIterator.HasNext()">
#DateFormat(
objIterator.Next(),
"ddd mmm d, yyyy"
)#<br />
</cfloop>
</p>
<!--- Test M-W-F strategy. --->
<!--- Create the date iterator. --->
<cfset objIterator = CreateObject(
"component",
"cfc.MWF"
).Init(
StartDate = "1/1/2009",
EndDate = "1/31/2009"
)
/>
<!--- Output dates. --->
<p>
<strong>Mon-Wed-Fri</strong>
</p>
<p>
<!--- Loop over dates. --->
<cfloop condition="objIterator.HasNext()">
#DateFormat(
objIterator.Next(),
"ddd mmm d, yyyy"
)#<br />
</cfloop>
</p>
<!--- Test second Tuesday strategy. --->
<!--- Create the date iterator. --->
<cfset objIterator = CreateObject(
"component",
"cfc.SecondTuesday"
).Init(
StartDate = "1/1/2009",
EndDate = "12/31/2009"
)
/>
<!--- Output dates. --->
<p>
<strong>Second Tuesday</strong>
</p>
<p>
<!--- Loop over dates. --->
<cfloop condition="objIterator.HasNext()">
#DateFormat(
objIterator.Next(),
"ddd mmm d, yyyy"
)#<br />
</cfloop>
</p>
What I really like about the concept of an interface (in general) is that while each test on the page creates it's own ColdFusion component, once inside the date iteration loop, the code is 100% uniform because it is expected that all concrete classes will uphold the same basic structure. When we run this test code, we get the following output:
EveryDay
Thu Jan 1, 2009
Fri Jan 2, 2009
Sat Jan 3, 2009
Sun Jan 4, 2009
Mon Jan 5, 2009
Tue Jan 6, 2009
Wed Jan 7, 2009
Thu Jan 8, 2009
Fri Jan 9, 2009
Sat Jan 10, 2009
Sun Jan 11, 2009
Mon Jan 12, 2009
Tue Jan 13, 2009
Wed Jan 14, 2009
Thu Jan 15, 2009Mon-Wed-Fri
Fri Jan 2, 2009
Mon Jan 5, 2009
Wed Jan 7, 2009
Fri Jan 9, 2009
Mon Jan 12, 2009
Wed Jan 14, 2009
Fri Jan 16, 2009
Mon Jan 19, 2009
Wed Jan 21, 2009
Fri Jan 23, 2009
Mon Jan 26, 2009
Wed Jan 28, 2009
Fri Jan 30, 2009Second Tuesday
Tue Jan 13, 2009
Tue Feb 10, 2009
Tue Mar 10, 2009
Tue Apr 14, 2009
Tue May 12, 2009
Tue Jun 9, 2009
Tue Jul 14, 2009
Tue Aug 11, 2009
Tue Sep 8, 2009
Tue Oct 13, 2009
Tue Nov 10, 2009
Tue Dec 8, 2009
Works like a charm! There will be more on this soon, but I really liked this first step. Going forward, I will probably dump the use of CFInterface as I don't feel that it adds any value; but, it was nice to actually have a working example on my site.
Want to use code from this post? Check out the license.
Reader Comments
@Ben,
You said it all at the beginning regarding CFInterface: "While I don't really need to use this"
I've yet to find a good example of when someone _should_ use CFInterface. It makes sense in Java-land, but not here in CF-Land (at least to me anyway).
@Steve,
I tend to agree. I pretty much threw it in there for funzies. Also, the fact that all the "actual" iteration classes extend a base class makes it seem even more silly to have a CFInterface set up.
Hey Ben,
Since it came up, I thought I would throw out a few suggestions about where I've found cf interface useful.
I generally use it anywhere the I'm writing object that need to be completely compatible and interchangeable in some way. I use the cfinterface to flag that to a developer that makes a new class and give them the stubs to implement their own.
A few examples of where I've used cfinterface are:
- Applications managing multiple caching services (all have similar functionality such as flushing assets or changing cache times, but their APIs are completely different) You could apply this to any service that multiple companies may offer
- A validation framework we created where CFCs act as extensions to the different check types that can be performed on fields. I use the interface to define several methods that must be there to get information about the validation rules, etc.
- A set of cfcs I have that do the job of formatting data dynamically based on conditions
In any of these cases I could have just as easily chosen not to use an interface and simply wrote them with the same method and with all hope, documented that for anyone who has to deal with the code. The reality is that I'm not the only developer in the world who uses my code and just as easily as I could write a vague or confusing comment telling developers that the consistency of these objects are important, I could write an interface. As a side benefit, the methods of an interface can be copied write out as stubs into an empty cfc to start you out quickly when making a new implementation.
Anyway, I was hoping that the CF team would do more creative things with interfaces in a dynamic language such as allow us to extend coldfusion's cfform validation via an interface implementation or other functionality.
@Mike,
I am glad that you are getting some good uses out of it. I think it makes more sense when you are working on teams, as it sounds like you are.