Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Gary Stanton
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Gary Stanton ( @SimianE )

Randomly Executing 1..N Nested ColdFusion Custom Tags

By on
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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel