Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Matt Vickers and Christian Ready
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Matt Vickers Christian Ready

RESwitch / RECase ColdFusion Custom Tags For Regular Expression Switch Statements

By
Published in Comments (5)

As you all know, I LOVE regular expressions. Pattern matching on strings is one of the most powerful weapons that I have in my bat-utility belt. The other day, I was in a situation where it would have been awesome to have a Switch statement that matched on patterns rather than string literals. Seeing as I've built just about every other kind of regular expression ColdFusion custom tag that I could think of, I figured why not give this a try.

The first thing I did was write out the calling page to codify how I wanted to the RESwitch and RECase ColdFusion custom tags to work. One of the most important aspects of pattern matching is being able to not only match patterns (obviously) but to be able to easily access the matched groups within the matched pattern. Here's what I came up with:

<!--- Set a value to test (quoted string). --->
<cfset strValue = """Hey Sugar""" />


<!--- Check to see what kind of value we are working with. --->
<cf_reswitch expression="#strValue#">

	<cf_recase
		pattern="^(\d+)\.(\d)+$"
		group1="intInteger"
		group2="intDecimal">

		Float found: #intInteger#.#intDecimal#.

	</cf_recase>

	<cf_recase pattern="^\d+$">

		Integer found: #strValue#.

	</cf_recase>

	<cf_recase
		pattern="^([^@]+)@(.+)\.(\w{2,})$"
		group1="strName"
		group2="strDomain"
		group3="strExtension">

		Email found: #strName#@#strDomain#.#strExtension#.

	</cf_recase>

	<cf_recase
		pattern="^(""([^""]+)""|([^""]+))$"
		group2="strQuotedValue"
		group3="strNonQuotedValue">

		<cfif Len( strQuotedValue )>

			Quoted string found: #strQuotedValue#.

		<cfelse>

			Non-Quoted string found: #strNonQuotedValue#.

		</cfif>

	</cf_recase>

	<!--- Default case. --->
	<cf_recase pattern="^[\w\W]*$">


		Unknown string value found: #strValue#.

	</cf_recase>

</cf_reswitch>

Here, you can see that like the ColdFusion CFSwitch tag, the RESwitch ColdFusion custom tag uses the Expression attribute. Then, the RECase tags use the Pattern attribute to define the pattern we'll be trying to match. Optionally, the RECase tag allows you to define variables into which the matched groups will be stored. This gives us not only the ability to test matches, but to easily access the components of those match.

Running the above test page, we get the following output:

Quoted string found: Hey Sugar.

Notice that even though our test value had quotes around it, we were able to pull out the actual string value by checking to see which group came back with a value. Ideally, a non-matched group would not even have a variable; however, to make this easier to use, I default the non-matched groups to the empty string to keep this a low-entry use tag.

The ColdFusion custom tags that make this happen are actually quite simple. Here is the RESwitch tag:

<!--- Check to see which tag mode we are in. --->
<cfif (THISTAG.ExecutionMode EQ "Start")>

	<!--- Start mode. --->

	<!--- Check to make sure this tag has an end tag. --->
	<cfif NOT THISTAG.HasEndTag>

		<cfthrow
			type="RESwitch.MissingEndTag"
			message="This RESwitch tag requires an end tag."
			/>

	</cfif>


	<!--- Param tag attributes. --->

	<!---
		This is the expression that we are testing (the value
		against which we are going to try and match our patterns).
	--->
	<cfparam
		name="ATTRIBUTES.Expression"
		type="string"
		/>


	<!---
		Set the match flag to indicate whether or not any of the
		child tags has successfully matched a tag.
	--->
	<cfset VARIABLES.IsPatternMatched = false />


	<!---
		Create a Pattern object that we can use to compile our
		patterns in the child tags.
	--->
	<cfset VARIABLES.PatternClass = CreateObject(
		"java",
		"java.util.regex.Pattern"
		) />

<cfelse>

	<!--- End mode. --->

</cfif>

And here is the RECase tag:

<!--- Check to see which tag mode we are in. --->
<cfif (THISTAG.ExecutionMode EQ "Start")>

	<!--- Start mode. --->

	<!--- Check to make sure this tag has an end tag. --->
	<cfif NOT THISTAG.HasEndTag>

		<cfthrow
			type="RECase.MissingEndTag"
			message="This RECase tag requires an end tag."
			/>

	</cfif>


	<!---
		Get a reference to the parent tag so that we can update
		the tag context as needed.
	--->
	<cfset VARIABLES.SwitchTag = GetBaseTagData( "cf_reswitch" ) />


	<!---
		Check to see if a pattern has already been matched. If
		so, then we want to exit out immediately.
	--->
	<cfif VARIABLES.SwitchTag.IsPatternMatched>

		<!---
			Exit out immediately - we don't want any more of the
			child case tags to execute.
		--->
		<cfexit method="exittag" />

	</cfif>


	<!---
		ASSERT: If we have made it this far then we know that
		no pattern has been matched against our expression yet.
	--->


	<!--- Param tag attributes. --->

	<!---
		This is the patten that we are going to test against
		the parent tag expression.
	--->
	<cfparam
		name="ATTRIBUTES.Pattern"
		type="string"
		/>


	<!--- Compile this regular expression patterns. --->
	<cfset VARIABLES.Pattern = VARIABLES.SwitchTag.PatternClass.Compile(
		JavaCast( "string", ATTRIBUTES.Pattern )
		) />

	<!---
		Get a matcher for this pattern as it is applied to the
		expression we are testing.
	--->
	<cfset VARIABLES.Matcher = VARIABLES.Pattern.Matcher(
		JavaCast(
			"string",
			VARIABLES.SwitchTag.ATTRIBUTES.Expression
			)
		) />


	<!--- Check to see if this matcher can find a match. --->
	<cfif VARIABLES.Matcher.Find()>

		<!---
			Update the parent flag to indicate that this tag has
			matched a pattern.
		--->
		<cfset VARIABLES.SwitchTag.IsPatternMatched = true />

		<!---
			Now that we have matched a pattern, let's see if
			we need to move any of the matched groups into
			tag attributes. I have picked 20 arbitrarily.
		--->
		<cfloop
			index="VARIABLES.GroupIndex"
			from="1"
			to="20"
			step="1">

			<!---
				Check to see if an attribute exists for this
				matched group.
			--->
			<cfif StructKeyExists( ATTRIBUTES, "group#VARIABLES.GroupIndex#" )>

				<!---
					There is chance that this group did not
					actually get matched in the pattern. If that
					is the case, getting it will return a NULL
					which will remove the variable.
				--->
				<cfset VARIABLES.GroupValue = VARIABLES.Matcher.Group(
					JavaCast( "int", VARIABLES.GroupIndex )
					) />

				<!--- Check to see if it was matched. --->
				<cfif StructKeyExists( VARIABLES, "GroupValue" )>

					<!--- Store the matched pattern. --->
					<cfset CALLER[ ATTRIBUTES[ "group#VARIABLES.GroupIndex#" ] ] = VARIABLES.GroupValue />

				<cfelse>

					<!---
						That pattern has this group, but it was
						NOT matched. Let's just store an empty
						string in the variable. Not the best
						strategy, but we can always tweak later.
					--->
					<cfset CALLER[ ATTRIBUTES[ "group#VARIABLES.GroupIndex#" ] ] = "" />

				</cfif>

			</cfif>

		</cfloop>

	<cfelse>

		<!---
			No match was found. Exit out of this tag and let
			the next child tag execute.
		--->
		<cfexit method="exittag" />

	</cfif>

<cfelse>

	<!--- End mode. --->

</cfif>

Notice that the matching algorithm makes use of the Java Pattern and Matcher classes. This means that you can make use of all of the functionality that is provided by the Java regular expression engine (which is more robust and significantly faster than the ColdFusion POSIX regular expression engine).

Want to use code from this post? Check out the license.

Reader Comments

15,848 Comments

@Francois,

Thanks man :) I am really gonna try to get back into it. Got some stuff in the queue that I want to play with. I think this was a good way to kick it off.

59 Comments

That is really nice!

I have had a few cases where I wanted to use cfswitch/cfcase, but I needed to check regular expressions instead of strings. I never thought of building a custom tag for it though.

I will definitely check this out next time I run into that situation.

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