Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (Jan. 2010) with: Jorge Hassan

RESwitch / RECase ColdFusion Custom Tags For Regular Expression Switch Statements

By Ben Nadel on
Tags: ColdFusion

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




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

Reply to this Comment

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.

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.