Randomly Executing 1..N Nested ColdFusion Custom Tags
After my post yesterday on the exciting work flow involved in randomly executing a nested ColdFusion custom tag, Ray Camden had some comments that inspired me to update the way the tag works. Rather than just randomly executing a single tag, I wanted to provide a way for the user to specify how many child tags to randomly execute. By default, it will execute just one; but, you can now specify any number of child tags or simply use the keyword, "all," to get the entire list to output in a random order:
<!--- Import the random switch tags. --->
<cfimport prefix="random" taglib="./" />
<!--- Select one of the following cases randomly. --->
<random:switch randomize="2">
<random:case>
(1) Hey there, how are you?<br />
</random:case>
<random:case>
(2) It's so nice to see you!<br />
</random:case>
<random:case>
(3) I'm sorry, I can't recall your name?<br />
</random:case>
<random:case>
(4) How's your mother doing?<br />
</random:case>
</random:switch>
Notice that the Switch.cfm tag now has a Randomize attribute. If you exclude this attribute, it defaults to 1 (one). You can specify a number, as I am doing in the demo, or you can pass in, "all." When we run the code above, we get our two randomly output values:
(4) How's your mother doing?
(3) I'm sorry, I can't recall your name?
Because we are executing more than one child tag, we need to update the way the tag works; rather than iterating once for collection and once for execution, the Switch.cfm custom tag now needs to iterate once for collection and as many times as is needed to execute the child tags. The number of post-collection iterations is not necessarily equal to the number of tags to execute. If two consecutive randomly chosen indexes are "in order", they will be executed in the same iteration. And so, the worst case scenario (completely backwards) is N iterations whereas the best case scenario (in original order) is one post-collection iteration.
The child tag, Case.cfm, has remained mostly unchanged. The only update I had to do was check the ExecutionMode of the tag before checking with the parent tag. This is something I technically should have done in yesterday's post; but, since there was no mutation of information, it didn't matter. Now, we are actually updating an internal collection with each call, so it becomes important to only perform the check in the proper mode:
Case.cfm
<!--- Only check execution in start mode. --->
<cfif (THISTAG.ExecutionMode EQ "Start")>
<!--- Get the parent tag reference. --->
<cfset VARIABLES.SwitchTag = GetBaseTagData( "cf_switch" ) />
<!--- Check to see if child should execute. --->
<cfif NOT VARIABLES.SwitchTag.ChildShouldExecute()>
<!---
The switch tag said not to execute, so exit out of
this tag before any code in the body can execute.
--->
<cfexit method="exittag" />
</cfif>
</cfif>
The main tag, Switch.cfm, on the other hand did have to be significantly updated. Rather than just using a single target index, since we might execute 1..N children, I have to build up a collection of child indexes and then randomly shuffle them. Then, rather than a single post-collection iteration, I have to keep iterating until I no longer have indexes left in my collection (I delete each index off the top of the collection as the given child tag executes).
Switch.cfm
<cffunction
name="ChildShouldExecute"
access="public"
returntype="boolean"
output="false"
hint="I expect to be called by a child tag and I return a boolean as to whether the given tag should execute.">
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Get the switch tag context. --->
<cfset var CONTEXT = GetBaseTagData( "cf_switch" ) />
<!---
Check to see which mode we are in. If we are collection,
then we only want the count and we don't want any child
tags to execute.
--->
<cfif (CONTEXT.SwitchMode EQ "Collection")>
<!--- Collection mode. --->
<!--- Add the new index to the index collection. --->
<cfset ArrayAppend(
CONTEXT.ChildIndexCollection,
(ArrayLen( CONTEXT.ChildIndexCollection ) + 1)
) />
<!---
Return false since we don't want any child tags to
execute at this point.
--->
<cfreturn false />
<cfelse>
<!--- Execution mode. --->
<!---
Now that we're in the execution mode, we have to keep
track of the number of tags that call this function.
We want to return false unless the given tag is at the
FRONT of the index collection.
--->
<!--- Increment child index. --->
<cfset CONTEXT.ChildIndex++ />
<!---
Check to see if the current index matches the
target index.
--->
<cfif (
ArrayLen( CONTEXT.ChildIndexCollection ) AND
(CONTEXT.ChildIndex EQ CONTEXT.ChildIndexCollection[ 1 ])
)>
<!--- Delete the first index. --->
<cfset ArrayDeleteAt(
CONTEXT.ChildIndexCollection,
1
) />
<!--- This is the correct tag. Let it execute. --->
<cfreturn true />
<cfelse>
<!--- This is not the target tag. Do not execute. --->
<cfreturn false />
</cfif>
</cfif>
</cffunction>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Check to see which mode we are executing. --->
<cfswitch expression="#THISTAG.ExecutionMode#">
<cfcase value="Start">
<!--- Param tag attributes. --->
<cfparam
name="ATTRIBUTES.Randomize"
type="string"
default="1"
/>
<!---
The randomize attribute determines how the child tags
get randomized. There are several possibilities
including keywords and numbers:
1 .. N: Outputs the given number of random children.
All: Outputs all in random order.
--->
<!---
Check to make sure that the ranomize attribute has a
valid value.
--->
<cfif NOT (
(ATTRIBUTES.Randomize EQ "All") OR
(
IsNumeric( ATTRIBUTES.Randomize ) AND
(Fix( ATTRIBUTES.Randomize ) EQ ATTRIBUTES.Randomize) AND
(ATTRIBUTES.Randomize GT 0)
))>
<!--- Bad param value. --->
<cfthrow
type="InvalidAttributeValue"
message="Randomize must be an integer greater than zero or ALL."
/>
</cfif>
<!---
In the start mode, we don't yet know how many
children we have. Therefore, we have to do one pass
over the child tags to gether the count before we
actually execute any of them.
--->
<cfset VARIABLES.SwitchMode = "Collection" />
<!---
Keep an array that will contain each index of the
child tag. At first, this will be bulit up, then it
will be randomized and leveraged.
--->
<cfset VARIABLES.ChildIndexCollection = [] />
<!---
Keep an index of the child tag that is running. This
will only come into play on the secondary passes when
we know which target index(es) we want to execute.
--->
<cfset VARIABLES.ChildIndex = 0 />
</cfcase>
<!--- ------------------------------------------------- --->
<cfcase value="End">
<!---
Check to see which mode we are in. If we are
collecting information or acting on it. If we are
collecting, it means that we have to randomize our
index collection. If not, we have to loop back until
we have no more indexes to execute.
--->
<cfif (VARIABLES.SwitchMode EQ "Collection")>
<!--- Randomize the index collection. --->
<cfset CreateObject( "java", "java.util.Collections" ).Shuffle(
VARIABLES.ChildIndexCollection
) />
<!---
Now that we have our child index collection
randomized, we have to see how many children we
need to execute. If we have all, keep all. If we
have a number, loop backwards, deleting indexes,
until we have the right count.
--->
<cfif (ATTRIBUTES.Randomize EQ 1)>
<!---
Since this it the most common case, optimize
for it by just overwriting the entire array.
--->
<cfset VARIABLES.ChildIndexCollection = [
VARIABLES.ChildIndexCollection[ 1 ]
] />
<cfelseif (ATTRIBUTES.Randomize NEQ "All")>
<!---
We are selecting 2..(N-1) values, so just
start looping backwards deleting them.
--->
<cfloop condition="(ArrayLen( VARIABLES.ChildIndexCollection ) GT ATTRIBUTES.Randomize)">
<!--- Delete last index. --->
<cfset ArrayDeleteAt(
VARIABLES.ChildIndexCollection,
ArrayLen( VARIABLES.ChildIndexCollection )
) />
</cfloop>
</cfif>
<!---
Change the mode to be execution rather than
collection. This will signal to the child tags
that its time to execute.
--->
<cfset VARIABLES.SwitchMode = "Execution" />
<!---
Loop back to body to allow one of the child tags
a chance to execute.
--->
<cfexit method="loop" />
<cfelse>
<!---
We just finished a mode of execution. Let's see if
we have any more child tag indexes to execute. If
so, loop back.
--->
<cfif ArrayLen( VARIABLES.ChildIndexCollection )>
<!--- Reset the child index. --->
<cfset VARIABLES.ChildIndex = 0 />
<!--- Run through tags again to execute next. --->
<cfexit method="loop" />
</cfif>
</cfif>
</cfcase>
</cfswitch>
Because one custom tag execution is the default, and the most likely case, I optimize for that after the index collection has been shuffled. But, if the user wants to execute 2 or more children, I have to loop backwards over the array, deleting indexes that are not relevant. Then, I simply keep looping until no more indexes are left in the collection.
I am not sure how useful a tag like this is, at least not with the more-than-one option. I think the real usefulness of this kind of exploration is just in seeing what kind of work flows can exist within ColdFusion custom tags. I think that custom tags are something that are under utilized, so hopefully seeing this kind of stuff might spark some excitement and some good conversation.
Want to use code from this post? Check out the license.
Reader Comments