Archive for the ‘UDF’ Category

Stripping out HTML tags from text with Coldfusion 9

Wednesday, April 7th, 2010

I’ve built a fairly simple UDF for stripping out HTML from a chunk of text. I’ll quickly explain my logic and what this UDF can do.

First, there are two types of tags in HTML. One is a wrapping tag. An example of this is the bold tag.

<b>Some content</b>

Next is a self-contained tag.

<img src="my/path/image.jpg" />

If the tag is a wrapping tag we need to make the decision of whether or not to delete the content the tag is wrapping. For example the content inside a bold tag we will most likely want to keep. While the content inside a script tag we will want to delete.

When it comes right down to it, we need to make three decisions about the HTML being deleted.

  1. What tag needs to be deleted.
  2. Is this a wrapping tag.
  3. If a wrapping tag, should the content be deleted.

So let’s take a look at the UDF.

<cfcomponent displayname="Strip HTML" hint="Strips out of a set of given text any specified html tags." output="false">

	<!--- ************************  STRIP HTML  ************************ --->
	<cffunction name="stripHtml" displayname="Strip HTML" description="Strips out specified HTML tags." access="public" output="false" returntype="struct">

		<!--- ARGUMENTS --->
		<cfargument name="text" displayName="Text" type="string" hint="Text to strip out html tags from." required="true" />
		<cfargument name="tags" displayName="Tags" type="string" hint="Tags to be striped from the text.  Ex. '[string:tag name],[what to remove - {string:tag | string:content}],[is it a wrapping tag? {boolean}]'. Tags are delimited with semi-colons." required="true" />

		<!--- SET SOME LOCAL VARS --->
		<cfset var textbytes = "">
		<cfset var counter = 1>
		<cfset var delete = false>
		<cfset var temp = "">
		<cfset var tagtoberemoved = "">
		<cfset var whatgetsremoved = "">
		<cfset var wrappingtag = "">

		<!--- BUILD STRUCT --->
		<cfset var data = structNew()>
		<cfset data.success = true>
		<cfset data.message = "">
		<cfset data.orginaltext = ARGUMENTS.text>
		<cfset data.strippedtext = ARGUMENTS.text>

		<!--- CHECK IF ALL CONTENT SHOULD BE REMOVED --->
		<cfif ARGUMENTS.tags eq "all">
			<!--- REMOVE HTML TAGS --->
			<cfset data.strippedtext = rereplaceNoCase(ARGUMENTS.text, "<[^>]*>", "", "all")>
		<cfelse>
			<!--- LOOP OVER THE LIST OF TAGS TO BE REMOVED --->
			<cfloop list="#ARGUMENTS.tags#" index="VARIABLES.i" delimiters=";">
				<!--- SET ATTRIBUTES OF TAG TO BE DELETED --->
				<cfset tagtoberemoved = listFirst(VARIABLES.i, ",")>
				<cfset whatgetsremoved = listGetAt(VARIABLES.i, 2, ",")>
				<cfset wrappingtag = listLast(VARIABLES.i, ",")>

				<!--- IF REMOVING JUST THE TAG --->
				<cfif whatgetsremoved eq "tag">
					<!--- CHECK IF IT IS A WRAPPING TAG --->
					<cfif wrappingtag eq true>
						<!--- REMOVE WRAPPING TAG, BUT NOT THE CONTENT --->
						<cfset data.strippedtext = rereplaceNoCase(data.strippedtext, "<#tagtoberemoved#>", "", "all")>
						<cfset data.strippedtext = rereplaceNoCase(data.strippedtext, "</#tagtoberemoved#>", "", "all")>
					<cfelse>
						<!--- REMOVE CONTAINED TAG --->
						<cfset data.strippedtext = rereplaceNoCase(data.strippedtext, "<#tagtoberemoved# />", "", "all")>
					</cfif>

				<!--- IF REMOVING TAG AND CONTENT --->
				<cfelseif whatgetsremoved eq "content">
					<!--- CHECK IF IT IS A WRAPPING TAG --->
					<cfif wrappingtag eq true>
						<!--- REMOVE THE TAG AND CONTENT --->
						<cfset data.strippedtext = rereplaceNoCase(data.strippedtext, "<#tagtoberemoved#>.*</#tagtoberemoved#>", "", "all")>
					<cfelse>
						<!--- REMOVE CONTAINED TAG --->
						<cfset data.strippedtext = rereplaceNoCase(data.strippedtext, "<#tagtoberemoved# />", "", "all")>
					</cfif>
				</cfif>
			</cfloop>
		</cfif>

		<!--- RETURN STRUCT --->
		<cfreturn data>

	</cffunction>

</cfcomponent>

The method takes two arguments. First is the chunk of text to have HTML stripped out of. Second is information about the HTML to strip out of the text. The latter argument accepts a string. Multiple tags can be specified by separating them with semi-colons. For each tag that needs deleted three pieces of information needs to be provided. Each of the three pieces are delimited with a comma. These are the three arguments listed above. First is the name of the tag. So if we are wanting to remove an img tag we would specify “img”, for bold “b”. Second, we specify if we want just the tag removed or the tag and the content it wraps. There are two acceptable strings here – “tag” (for just removing the tag) and “content” (for removing the tag and the content). Lastly, specify whether this is a wrapping or contained tag. For an image tag we would specify “false” because it is not a wrapping tag. For bold we would specify “true” because it is a wrapping tag.

One last note, if you want to strip out all HTML tags you can just pass in the string “all” to the method. But note that this will just strip out the HTML and any content being wrapped by the tags will be left alone.

So let’s look at an example of how to call this tag.

<!--- TEXT TO STRIP HTML FROM --->
<cfset VARIABLES.text = "Lorem <img />ipsum <b>dolor</b> sit amet, <em>consectetur</em> adipiscing elit.>

<!--- STRIP OUT ALL HTML --->
<cfset stripHtml(VARIABLES.text, "all")>

<!--- STRIP OUT IMG, B, AND EM TAGS --->
<cfset stripHtml(VARIABLES.text, "img,tag,false;b,tag,true;em,content,true")>

Feel free to copy this and use it as you please.

Random Password Generater UDF

Tuesday, March 16th, 2010

I realize that the web is full of password generators, but thought I’d add to the clutter with my own. It’s fairly simple with a few user-defined options for how the password is generated. Also you’ll need Coldfusion to run this method. Let’s take a quick look at the method, and as always feel free to use the method. Just drop it into a cfc, or on the page if that’s what fits your application best, call it with any special instructions via the arguments, and it will return back a struct with the appropriate data.

password_generator.cfc

<cfcomponent displayname="Generate Password" hint="Generates a random password." output="false">

	<!--- ************************  GENERATE PASSWORD  ************************ --->
	<cffunction name="generatePassword" displayname="Generate Password" description="Generates a random password." access="public" output="false" returntype="struct">

		<!--- ARGUMENTS --->
		<cfargument name="pwdlength" displayName="Password Length" type="numeric" default="10" required="false" />
		<cfargument name="includenumbers" displayName="Include Numbers" type="boolean" hint="Whether or not to include numbers in password." default="true" required="false" />
		<cfargument name="includeuppercase" displayname="Include Upper Case" type="boolean" hint="Whether or not to include uppercase characters." default="false" required="false" />
		<cfargument name="hashpwd" displayname="Hash Password" type="boolean" hint="Whether or not a hashed version of the password should be returned." default="false" required="false" />
		<cfargument name="hashtype" displayname="Hash Type" type="string" hint="Type of hash to perform." default="SHA-256" required="false" />

		<!--- SET CHARACTER RANGES --->
		<cfset var number = "48,57">
		<cfset var uppercase = "65,90">
		<cfset var lowercase = "97,122">

		<!--- SET A COUNTER --->
		<cfset var counter = 1>

		<!--- BUILD STRUCT --->
		<cfset var data = structnew()>
		<cfset data.passwordraw = "">
		<cfset data.passwordhashed = "">

		<cfscript>
			/* WHILE THE PASSWORD LENGTH IS LESS THAN THE SPECIFIED LENGTH */
			do
			{
				/* RETURN A RANDOM CHARACTER */
				character = randrange(listfirst(number, ","), listlast(lowercase, ","));

				/* IF THE CHARACTER IS LOWERCASE */
				if(character gte listfirst(lowercase, ",") AND character lte listlast(lowercase, ","))
				{
					/* SET CHARACTER INTO STRING */
					data.passwordraw = data.passwordraw & chr(character);
				}

				/* CAN WE USE UPPERCASE CHARACTERS */
				if(ARGUMENTS.includeuppercase eq true)
				{
					/* IF CHARACTER IS UPPERCASE */
					if(character gte listfirst(uppercase, ",") AND character lte listlast(uppercase, ","))
					{
						/* SET CHARACTER INTO STRING */
						data.passwordraw = data.passwordraw & chr(character);
					}
				}

				/* CAN WE USE NUMBERS */
				if(ARGUMENTS.includenumbers eq true)
				{
					/* IF CHARACTER IS NUMBER */
					if(character gte listfirst(number, ",") AND character lte listlast(number, ","))
					{
						/* SET CHARACTER INTO STRING */
						data.passwordraw = data.passwordraw & chr(character);
					}
				}
			}
			while(len(data.passwordraw) lt ARGUMENTS.pwdlength);
		</cfscript>

		<!--- SHOULD WE HASH THE PASSWORD --->
		<cfif ARGUMENTS.hashpwd eq true>
			<cfset data.passwordhashed = hash(data.passwordraw, ARGUMENTS.hashtype, "us-ascii")>
		</cfif>

		<cfreturn data>

	</cffunction>

</cfcomponent>

It takes five arguments. First is the length of the password you want to generate. So do you want it to be 4 characters, 10 characters, or whatever. Second, do you want numbers to be included in the password? If so set it to true. This defaults to true if not specified. Third, do you want any letters that are uppercased? If using a case sensitive password you may wish to specify this to true. It defaults to false. Fourth, do you want to also return a hashed version of the password. If storing in a database you may want this. And if you are returning a hashed version, which algorithm should be used in the hash process. This defaults to “SHA-256″. You can use any algorithm supported by Coldfusion.

Next I set some variables that hold the lowest and highest byte code representing numbers, uppercase, and lowercase according to ascii. Build the struct to return, and then set some script that will pump out our random password according to the specifications.

Lastly, if the password should be hashed it is.

Enjoy.

Logging complex data with Coldfusion and JSON

Tuesday, March 2nd, 2010

Logging with Coldfusion is very simple, but in and of itself a little limiting. Generally the log itself is a string. So while logs are certainly useful for recording an action and when it happened the amount of data that is presented in the log file is fairly restrictive without doing a whole lot of work, and even then your log file may look something like: “Error occured on page #thispage.cfm# by user #john smith#. The error catch message is #an error message#. It occurred on line #230#. I’ve wrapped the dynamic content with hashes. On top of all that we’re really not saying that much here. The thing is Coldfusion gives us a ton of data nicely organized as a struct, if only we could log it.

This is just another place where JSON becomes very handy for the developer.  By serializing the struct as JSON we can log the struct as a string.  If we need to see the struct we simply need to grab the JSON and deserialize it in which case the struct will be dumpable again.  Also for debugging purposes we developers often email ourselves errors when they occur.  This is a good practice, one that I do for all my applications.  Once an application goes into production the idea is that you shouldn’t be getting that many email errors, but during development (if you’re like me) you’ll get an error email about every 3 minutes.  As you can imagine emailing a struct is a significantly larger file size than sending that same struct as JSON.  I emailed the test struct below with coldfusion as both a struct and as JSON.  The struct html was 14.4 kb.  The JSON was 0.8 kb.  That means that I can make 18 more errors for every one error that I can make now before filling up the server.

Screen shot 2010-03-02 at 7.56.24 PM

Test Struct

I created the above struct because it contains the main data types.  You can see it is a struct that contains an array, an integer, another struct, a string, and a boolean.  The struct can be created with the following code.

<cfset var data = structnew()>
<cfset data.success = true>
<cfset data.message = "This is a message.">
<cfset data.anumber = 101>
<cfset data.anarray = arrayNew(1)>
<cfset data.astruct = structnew()>

<cfloop from="1" to="10" index="VARIABLES.i">
	<cfset arrayAppend(data.anarray, VARIABLES.i)>
	<cfset data.astruct[VARIABLES.i] = VARIABLES.i + 5>
</cfloop>

(more…)

URL Format Manager UDF

Tuesday, March 2nd, 2010

I’ve worked up a pretty basic UDF for formatting a URL.  If you have an application that allows users to input a url you may notice that they have about a million different ways of entering it.  What we as developers need however is a full address.  That means we need the ‘http://’ included.  Most user’s however, would never include this in their entered URL.

This UDF will detect how much of a URL the user entered, and set the formatting as is needed.  It accepts three arguments.  First is the URL to be formatted.  Second is a boolean specifying whether or not to append ‘http://’ if necessary.  Third is a boolean specifying whether or not to append ‘www.’ if needed.  If the second argument is set to true the third argument will be set to true by default.  This is based on the assumption that if you have ‘http://’ then you would also want ‘www.’.  Remember that this is a fairly basic method.  It will detect secure protocols, but will not detect subdomains and such.  It also pays no attention to the ‘.com’, ‘.org’, etc.  So feel free to use it if it fits your needs.

<!--- ********************************  FORMAT URL  ******************************** --->
<cffunction name="formatUrl" displayname="Format URL" description="Formats a URL for appropriate web use." access="public" output="false" returntype="struct">

	<!--- PASS IN ARGUMENTS --->
	<cfargument name="url" displayName="URL" type="string" hint="URL to be formated" required="true" />
	<cfargument name="includehttp" displayName="Include HTTP" type="boolean" hint="Indicate whether or not to include HTTP" default="true" required="false" />
	<cfargument name="includewww" displayName="Include www" type="boolean" hint="Whether or not to include the www. If includehttp is true this must also be true." default="true" required="false" />

	<!--- SET SOME LOCAL VARS --->
	<cfset var charcontainer = "">
	<cfset var protocol = "http://">
	<cfset var protocollength = 7>

	<!--- CREATE STRUCT --->
	<cfset var data = structnew()>
	<cfset data.success = true>
	<cfset data.message = "">
	<cfset data.url = ARGUMENTS.url>

	<cftry>
		<!--- CHECK IF SSL IS USED --->
		<cfif left(trim(data.url), 8 ) eq "https://">
			<cfset protocol = "https://">
			<cfset protocollength = 8>
		</cfif>

		<!--- IF ARGUMENTS.INCLUDEHTTP IS TRUE MAKE SURE ARGUMENTS.INCLUDEWWW IS TRUE ALSO --->
		<cfswitch expression="#ARGUMENTS.includehttp#">
			<!--- ADD HTTP:// AND WWW. IF NECESSARY --->
			<cfcase value="true">
				<!--- SET ARGUMENTS.INCLUDEWWW TO TRUE --->
				<cfset ARGUMENTS.includewww = true>

				<!--- CHECK IF HTTP INCLUDED IN URL --->
				<cfif left(trim(data.url), protocollength) neq protocol>
					<!--- CHECK IF THE WWW. IS INCLUDED --->
					<cfif left(trim(data.url), 4) eq "www.">
						<cfset data.url = protocol & data.url>
					<cfelse>
						<cfset data.url = protocol & "www." & data.url>
					</cfif>
				<cfelse>
					<!--- REMOVE THE HTTP, IT WILL BE ADDED BACK --->
					<cfset data.url = replace(data.url, protocol, "", "one")>

					<!--- REMOVE THE WWW. --->
					<cfset data.url = replace(data.url, "www.", "", "one")>

					<!--- CHECK IF WWW INCLUDED IN URL --->
					<cfset charcontainer = left(trim(data.url), 4 + protocollength)>

					<cfif right(trim(charcontainer), 4) neq "www.">
						<cfset data.url = protocol & "www." & data.url>
					</cfif>
				</cfif>
			</cfcase>

			<!--- REMOVE HTTP:// --->
			<cfdefaultcase>
				<!--- CHECK IF HTTP INCLUDED IN URL --->
				<cfif left(trim(data.url), protocollength) eq protocol>
					<cfset data.url = replace(data.url, protocol, "", "one")>

					<!--- CHECK IF WWW. SHOULD BE INCLUDED --->
					<cfswitch expression="#ARGUMENTS.includewww#">
						<!--- MAKE SURE WWW. IS INCLUDED --->
						<cfcase value="true">
							<cfif left(trim(data.url), 4) neq "www.">
								<cfset data.url = "www." & data.url>
							</cfif>
						</cfcase>

						<!--- REMOVE WWW. IF NECESSARY --->
						<cfdefaultcase>
							<cfif left(trim(data.url), 4) eq "www.">
								<cfset data.url = replace(data.url, "www.", "", "one")>
							</cfif>
						</cfdefaultcase>
					</cfswitch>
				<cfelse>
					<!--- CHECK IF WWW. SHOULD BE INCLUDED --->
					<cfswitch expression="#ARGUMENTS.includewww#">
						<!--- MAKE SURE WWW. IS INCLUDED --->
						<cfcase value="true">
							<cfif left(trim(data.url), 4) neq "www.">
								<cfset data.url = "www." & data.url>
							</cfif>
						</cfcase>

						<!--- REMOVE WWW. IF NECESSARY --->
						<cfdefaultcase>
							<cfif left(trim(data.url), 4) eq "www.">
								<cfset data.url = replace(data.url, "www.", "", "one")>
							</cfif>
						</cfdefaultcase>
					</cfswitch>
				</cfif>
			</cfdefaultcase>
		</cfswitch>

		<cfcatch type="any">
			<cfset data.success = false>
			<cfset data.message = "There was a problem formatting the url.">
		</cfcatch>
	</cftry>

	<!--- RETURN STRUCT --->
	<cfreturn data>

</cffunction>

Phone Number Compressor Method

Friday, February 26th, 2010

This is part 1 of a 2 part post.  In this part (part 1) we will compress a phone number for database storage.  But what good is a compressed phone number stored away the remote darks of some database?  This is where part 2 comes in.  In that part we will break our phone number back up into its three parts to be formatted for display.  More on that in a couple hours.  First let’s compress it.

I built this UDF because I got tired of always manually having to add in special characters to remove from the original user entered phone number.  Every time the client decided to type their phone number into the form field with new special characters I had to make an update I couldn’t charge for.  No fun!  After a few of these updates I decided it would be worth just writing a UDF that I could implement and forget forever.  The client could write their phone numbers in any format they wished and no matter how off-the-wall this UDF would remove everything except the number itself.

A phone number is really just a sting of ten numbers unless you include the ‘1′ at the beginning, and I don’t – so just ten numbers. (On this note, I should state that this UDF works only with U.S. phone numbers.)  These ten numbers, and only these ten numbers is what we want to store as raw data.  If we want the maximum front-end formatting flexibility we need to start with raw data, not pre-formatted phone number of someone you don’t even know.

As such, let’s look at the UDF.  It’s posted here in its entirety so feel free to copy and paste it into your own CFC.

<cffunction name="compressPhoneNumber" displayname="Compress PhoneNumber" description="Compresses a phone number to remove formatting." access="public" output="false" returntype="struct">

	<!--- PASS IN NUMBER TO BE COMPRESSED --->
	<cfargument name="phonenumber" displayName="Phone Number" type="string" hint="Phone Number to be compressed." required="true" />

	<!--- BUILD STRUCT --->
	<cfset var number = structnew()>
	<cfset number.compressednumber = ARGUMENTS.phonenumber>
	<cfset number.originalnumber = ARGUMENTS.phonenumber>
	<cfset number.charary = arrayNew(1)>
	<cfset number.specialchars = arrayNew(1)>
	<cfset number.success = true>
	<cfset number.message = "">

	<cftry>
		<!--- CREATEA BYTE ARRAY OF PASSED IN STRING --->
		<cfset number.charary = ARGUMENTS.phonenumber.GetBytes()>
		<cfset number.aryLength = arrayLen(number.charary)>

		<!--- LOOP OVER ARRAY --->
		<cfloop from="1" to="#number.aryLength#" index="VARIABLES.i">
			<!--- IF NOT A NUMBER SET CHARACTER INTO A LIST --->
			<cfif number.charary[VARIABLES.i] lt 48 OR number.charary[VARIABLES.i] gt 57>
				<cfset arrayAppend(number.specialchars, chr(number.charary[VARIABLES.i]))>
			</cfif>
		</cfloop>

		<!--- LOOP OVER SPECIAL CHARACTERS ARRAY AND REPLACE IT IN THE ORIGINAL STRING --->
		<cfloop array="#number.specialchars#" index="VARIABLES.i">
			<cfset number.compressednumber = replace(number.compressednumber, VARIABLES.i, "", "all")>
		</cfloop>

		<!--- CATCH ANY ERRORS --->
		<cfcatch type="any">
			<cfset number.success = false>
			<cfset number.message = "There was a problem compressing the number.  Please refresh the page and try again.">
		</cfcatch>
	</cftry>

	<!--- RETURN STRUCT --->
	<cfreturn number>

</cffunction>

The method needs one thing and one thing only.  The phone number to be compressed or have the extra formatting removed.  Nothing too complicated happens with the number we pass in.  The gist of it is that we need to determine what is not a number and then replace it with nothing.  Although there are a couple ways to break a string down into its parts and pieces I prefer to break them down into their corresponding bytes.  This way we can determine characters according to their byte code.  Also another reason for using this method is the handy Java GetBytes() method.  Although undocumented in Coldfusion you can dig down into the underlying Java and use its methods to do your bidding.  In our case the GetBytes() method returns an array characters in byte format of the string that was passed to it.  Just remember that Java is a heavily typed language and as such make sure that you use the toString() method to ensure that your phone number is string and not a number as it was intended to be.  If for some reason you later needed it to be typed back as a number you can always use Java’s ParseInt() method to return it to a number, although this is probably not advisable for formatting reasons.

Moving on, we now have an array of byte code.  Perfect.  If we look at our ascii table of character codes we’ll see that the number ‘0′ is represented as ‘48′ in byte code and the number ‘9′ is represented as ‘57′ in byte code, and the other digits fall in-between.  All we need to do is loop over the bytes, check if they fall between ‘48′ and ‘57′.  If not we ditch them leaving us with just the number itself as a string.

I return the data in a struct that shows the original number, special characters that were removed from the string, and the compressed string.  You could of course remove this struct and just return the string for a little performance gain, but thought you might like to see what’s happening behind the scenes a little.

Phone Number Divider Method

Thursday, February 25th, 2010

This is part 2 of a previous post on using a phone number compression UDF.  Here we will look at a UDF I built that will take that compressed phone number from the database or wherever and break it up into its area code, prefix, and line number.  We need to break up our phone number for formatting purposes.  For example our compressed number looks like ‘8005559876′ and we need it to look like ‘(800) 555-9876′. This is actually really simple, so let’s just look at the UDF.  Feel free to use this in your own CFC.

<!--- *****************************  DIVID PHONE NUMBER  ***************************** --->
<cffunction name="divide" displayname="Divide Phone Number" description="Divides a string of 10 digits into three parts." access="public" output="false" returntype="struct">

	<!--- PASS IN NUMBER TO BE DIVIDED --->
	<cfargument name="phonenumber" displayName="Phone Number" type="string" hint="Phone number to be divided." required="true" />

	<!--- BUILD STRUCT --->
	<cfset var number = structnew()>
	<cfset number.areacode = "">
	<cfset number.prefix = "">
	<cfset number.linenumber = "">
	<cfset number.success = true>
	<cfset number.message = "">

	<!--- MAKE SURE NUMBER PASSED IN 10 DIGITS --->
	<cfif len(trim(ARGUMENTS.phonenumber)) eq 10>
		<cftry>
			<!--- AREACODE --->
			<cfset number.areacode = left(ARGUMENTS.phonenumber, 3)>

			<!--- PREFIX --->
			<cfset number.prefix = left(ARGUMENTS.phonenumber, 6)>
			<cfset number.prefix = right(number.prefix, 3)>

			<!--- LINE NUMBER --->
			<cfset number.linenumber = right(ARGUMENTS.phonenumber, 4)>

			<!--- CATCH ANY ERRORS --->
			<cfcatch type="any">
				<cfset number.success = false>
				<cfset number.message = "There was a problem dividing the number.  Please refresh the page and try again.">
			</cfcatch>
		</cftry>
	<cfelse>
		<cfset number.success = false>
		<cfset number.message = "The number passed in did not contain 10 digits.  Please refresh the page and try again.">
	</cfif>

	<!--- RETURN STRUCT --->
	<cfreturn number>

</cffunction>

We’re just using the right() and left() functions to get the characters we want.  The left three characters will always be our area code.  The right four characters will always be our line number.  And the fourth, fifth, and sixth numbers will always be our prefix.  Stick them in a struct, return it, and format it to your hearts content on the front-end.

String Truncation Method

Sunday, February 21st, 2010

I’ve worked up a UDF that will truncate a string according to a set of rules defined by the user.  My reasoning for building this is that it seems like every time I need to truncate data it’s being truncated differently.  One client wants it truncated to a certain letter, others a certain word, or a sentence.  God knows that next it’ll be the nearest asterisk or dash or some other off-the-wall character.  So I came up with a set of rules I thought would let me define my truncation methods for the future without having to change any of the foundation code.  So first I’ll just post the method.

<!--- *****************************  TRUNCATE TO WORD  ***************************** --->
<cffunction name="truncateToChar" displayname="Truncate To Word" description="Truncates a string to a certain character length at the next or previous word." access="public" output="false" returntype="struct">

	<!--- PASS IN ARGUMENTS --->
	<cfargument name="stringtobetruncated" displayName="String to be Truncated" type="string" hint="This is the string that will be truncated." required="true" />
	<cfargument name="numofcharacters" displayName="Number of Characters" type="numeric" hint="Number of characters to truncate string at." required="false" default="300" />
	<cfargument name="beforeafter" displayName="Before or After" type="string" hint="To truncate to the nearest word before or after the character cutoff limit." required="false"  default="before" />
	<cfargument name="showellipses" displayname="Show Ellipses" type="boolean" hint="Whether or not to show ellipses at the end of the truncated data." required="false" default="true" />
	<cfargument name="chartotruncateto" displayname="Character to Truncate To" type="string" hint="This is the character that the program will truncate to." required="false" default="">

	<!--- SET SOME LOCAL VARS --->
	<cfset var counter = 1>
	<cfset var dataarray = arrayNew(1)>
	<cfset var truncatechar = "">
	<cfset var charindex = ARGUMENTS.numofcharacters>

	<!--- CREATE A STRUCT --->
	<cfset var data = structnew()>
	<cfset data.success = true>
	<cfset data.message = "">
	<cfset data.originalstring = ARGUMENTS.stringtobetruncated>
	<cfset data.truncatedstring = "">
	<cfset data.numofchars = 0>
	<cfset data.maxchars = 0>
	<cfset data.ischar = "">

	<!--- TRY TRUNCATING THE DATA --->
	<cftry>
		<!--- CONVERT STRING TO AN ARRAY OF CHARACTERS --->
		<cfset dataarray = ARGUMENTS.stringtobetruncated.GetBytes()>
		<cfset data.numofchars = arrayLen(dataarray)>
		<cfset data.maxchars = data.numofchars - 1>

		<!--- MAKE SURE NUMBER OF CHARACTERS IS GREATER THAN 0 AND LESS THAN THE NUMBER OF CHARACTERS IN THE STRING --->
		<cfif ARGUMENTS.numofcharacters lt 1 OR ARGUMENTS.numofcharacters gt data.numofchars>
			<cfset data.success = false>
			<cfset data.message = "Please enter a number for characters that falls between 0 and #data.numofchars#.">
		<cfelse>
			<!--- FIND THE CHARACTER AT THE SPECIFIED CHARACTER NUMBER --->
			<cfset truncatechar = chr(dataarray[ARGUMENTS.numofcharacters])>

			<!--- CHECK IF NO CHARACTER TO TRUNCATE TO IS SPECIFIED --->
			<cfif ARGUMENTS.chartotruncateto eq "" OR truncatechar eq ARGUMENTS.chartotruncateto>
				<!--- IF SO SET THE TRUNCATED STRING INTO STRUCT --->
				<cfset data.truncatedstring = left(ARGUMENTS.stringtobetruncated, ARGUMENTS.numofcharacters)>

				<!--- SHOW ELLIPSES IF NEEDED --->
				<cfif ARGUMENTS.showellipses eq true>
					<cfset data.truncatedstring = data.truncatedstring & " ...">
				</cfif>
			</cfif>

			<!--- MAKE SURE THE CHARACTER DIDN'T FALL AT END OF WORD BEFORE CONTINUING --->
			<cfif data.truncatedstring eq "">
				<!--- CHECK IF DATA SHOULD BE TRUNCATED TO PREVIOUS OR NEXT WORD --->
				<cfswitch expression="#ARGUMENTS.beforeafter#">
					<!--- WORD BEFORE --->
					<cfcase value="before">
						<!--- BEGIN MOVING BACKWARDS CHARACTER BY CHARACTER UNTIL WE REACH A SPACE --->
						<cfloop from="1" to="#ARGUMENTS.numofcharacters#" index="VARIABLES.i">
							<!--- LOWER CHARACTER INDEX BY 1 --->
							<cfset charindex = charindex - 1>

							<!--- MAKE SURE THAT THE CHARACTER TO TRUNCATE TO EXISTS --->
							<cfif charindex eq 0>
								<!--- JUST TRUNCATE TO CHARACTER INDEX --->
								<cfset data.truncatedstring = left(ARGUMENTS.stringtobetruncated, ARGUMENTS.numofcharacters)>
							<cfelse>
								<!--- GET THE CHARACTER AT NEW INDEX --->
								<cfset data.ischar = chr(dataarray[charindex])>

								<!--- IF THE CHARACTER STOP LOOPING --->
								<cfif data.ischar eq ARGUMENTS.chartotruncateto>
									<!--- TRUNCATE AT SELECTED CHARACTER INDEX --->
									<cfset data.truncatedstring = left(ARGUMENTS.stringtobetruncated, charindex)>

									<cfbreak>
								</cfif>
							</cfif>
						</cfloop>
					</cfcase>

					<!--- WORD AFTER --->
					<cfcase value="after">
						<!--- BEGIN MOVING BACKWARDS CHARACTER BY CHARACTER UNTIL WE REACH A SPACE --->
						<cfloop from="1" to="#ARGUMENTS.numofcharacters#" index="VARIABLES.i">
							<!--- LOWER CHARACTER INDEX BY 1 --->
							<cfset charindex = charindex + 1>

							<!--- MAKE SURE THAT THE CHARACTER TO TRUNCATE TO EXISTS --->
							<cfif charindex gte data.maxchars>
								<!--- JUST TRUNCATE TO CHARACTER INDEX --->
								<cfset data.truncatedstring = left(ARGUMENTS.stringtobetruncated, ARGUMENTS.numofcharacters)>
							<cfelse>
								<!--- GET THE CHARACTER AT NEW INDEX --->
								<cfset data.ischar = chr(dataarray[charindex])>

								<!--- IF THE CHARACTER STOP LOOPING --->
								<cfif data.ischar eq ARGUMENTS.chartotruncateto>
									<!--- TRUNCATE AT SELECTED CHARACTER INDEX --->
									<cfset data.truncatedstring = left(ARGUMENTS.stringtobetruncated, charindex)>

									<cfbreak>
								</cfif>
							</cfif>
						</cfloop>

						<!--- TRUNCATE AT SELECTED CHARACTER INDEX --->
						<cfset data.truncatedstring = left(ARGUMENTS.stringtobetruncated, charindex)>
					</cfcase>

					<!--- IF EITHER "BEFORE" OR "AFTER" WAS NOT PASSED IN --->
					<cfdefaultcase>
						<cfset data.success = false>
						<cfset data.message = "An inappropriate befor/eafter argument was passed in: #ARGUMENTS.beforeafter#.">

						<!--- THROW ERROR --->
						<cfthrow message="#data.message#">
					</cfdefaultcase>
				</cfswitch>

				<!--- SHOW ELLIPSES IF NEEDED --->
				<cfif ARGUMENTS.showellipses eq true>
					<cfset data.truncatedstring = data.truncatedstring & " ...">
				</cfif>
			</cfif>
		</cfif>

		<!--- CATCH ANY ERRORS --->
		<cfcatch type="any">
			<cfset data.success = false>
			<cfset data.message = "There was a problem truncating the data.">
		</cfcatch>
	</cftry>

	<!--- RETURN STRUCT --->
	<cfreturn data>

</cffunction>

Also feel free to copy and use this.  That’s really the point of posting it.  We have five arguments or parameters here.  First we need to make sure we tell the method which string is being truncated.  Second, at which character should the data be truncated.  Third, if truncating to a certain type of character such as a space for a word or a period for a sentence should it be truncated to the nearest character before or after the cutoff character.  Two acceptable strings are accepted here: “before” and “after”.  Fourth, should ellipses be appended to the end of the truncated string.  This is ‘true’ if yes and ‘false’ if no.  And lastly, the type of character that the truncation should shoot for.  If this is left as an empty string it will truncate to the cutoff letter.

For example we have this string: “The brown fox. The grey dog.”.  We truncate to the character 5 with no type of character to truncate to.  The truncation would look like “The b …”.  Remember that spaces are considered characters here.  Now we do the same truncation, but tell it to truncate to the nearest space (” “) “before” the cutoff character.  Our cutoff character is still the “b” in ‘brown’, but this time our truncation will return “The …”.  If we specified the same thing but “after” the cutoff character our truncation string would look like “The brown …”.  We can also do sentences by passing in the type of character as a period (“.”).  If we passed this in for “after” the cutoff character our truncation would return “The brown fox. …”.  You could cut off to any character just by passing in that character to the ‘chartotruncateto’ argument.

Lastly, just keep in mind that if you pass in an asterisk as the cutoff character and there is no asterisk in the character it will truncate the string to the specified character count.  So again, if we told the method to truncate to the 5th character with a character type of asterisk (“*”) after the cutoff character the truncation would look like “The b …”.  This is because there is no asterisk.  And one more thing.  Obviously if you pass in a cutoff character number greater than the number of characters in the string or less than 1 it will just not truncate the string.

Finally I’ve set it up to return a struct called data that contains some of the data that is being passed around and manipulated in the method.  You could of course choose to just return the string, but thought it might be nice if you could see some of the inner workings of the method.  Enjoy.

Checking if a number is even or odd with Coldfusion 8

Tuesday, January 12th, 2010

I was dynamically building a two-column table this morning based on a query result set.  I created the table just fine, but realized that when my query returned an odd number of records it would not show the last row.  When I bumped up my loop by one it showed the table correctly when an odd number of records were returned, but showed an extra table row when an even number of table records were returned.

Clearly, what I needed was a way to determine if the number of records returned back was even or odd.  Once I knew this I could handle the data appropriately.  There’s no predefined methods in Coldfusion 8 that I know of that checks whether a number is even or odd.

So I wrote up this little UDF.  It takes one parameter which is the number to be checked.

I’ve pasted the function below.  Feel free to change it to fit your needs.  It’s super simple.  All it really does is divide the number to be checked in half and checks to see if the result is a decimal or not.

We know a couple things.  First, regardless of how you define and integer, we all agree it is not a decimal.  And second, that any odd number divided by two will always yield a decimal, while any even number divided by two will always yield an integer.  This difference is what we check for.

The function returns a struct. It will always return a success equal to true or false, and a message indicated the results.  The message may also indicate the problem if it fails validation.

The function does do a little bit of validation.  It checks to make sure that the number being checked is a valid integer.  So a decimal or letter will return an error in the message.  If it passes validation a third result is given which is ‘iseven’.  This will return true if even and false if odd.

Hope it helps.

<cffunction name="isEven" displayname="Is Even" description="Determines if a number is even or odd." access="public" output="false" returntype="struct">

	<!--- PASS IN NUMBER TO CHECK --->
	<cfargument name="numbertocheck" displayname="Number to Check" hint="This is the number to check if even or not." type="numeric" required="true" />

	<!--- CREATE STRUCT --->
	<cfset VARIABLES.result = structnew()>
	<cfset VARIABLES.result.success = true>
	<cfset VARIABLES.result.message = "Your number is even.">
	<cfset VARIABLES.result.iseven = true>

	<!--- MAKE SURE THAT NUMBER PASSED IN IS AN INTEGER TO BEGIN WITH --->
	<cfif isvalid("integer", ARGUMENTS.numbertocheck)>
		<!--- GET RESULT OF NUMBER DIVIDED BY TWO --->
		<cfset VARIABLES.divided = ARGUMENTS.numbertocheck / 2>

		<!--- CHECK IF DIVIDED NUMBER IS NOT AN INTEGER --->
		<cfif NOT isvalid("integer", VARIABLES.divided)>
			<!--- RESET DATA IN STRUCT TO REFLECT AN ODD NUMBER --->
			<cfset VARIABLES.result.message = "Your number is odd.">
			<cfset VARIABLES.result.iseven = false>
		</cfif>
	<cfelse>
		<cfset VARIABLES.result.success = false>
		<cfset VARIABLES.result.message = "Please pass in a valid integer to check.">
	</cfif>

	<!--- RETURN THE STRUCT --->
	<cfreturn VARIABLES.result>

</cffunction>