Creating A Struct From A ColdFusion Array Using The TreeMap And The LinkedHashMap

Posted December 7, 2010 at 10:23 AM by Ben Nadel

Tags: ColdFusion

The other day, I was reading about Railo's ability to treat arrays as if they were structs. Specifically, Railo can execute a collection loop on an array in order to iterate over its defined indicies in order. In ColdFusion, we can of course use a FOR loop or an array loop to accomplish such things; but, there is something very nice about being able to loop over nothing but the defined indicies of an array. I wanted to see if I could mimic this behavior in Adobe ColdFusion by converting an array into a struct.

Arrays are ordered; structs are not. As such, we can't simply convert an array to a name-value collection. Doing so would be fine during insertion; but it would become unpredictable during iteration. In order to maintain iteration order for our array-as-struct representation, we need a way to maintain order within our collection.

Recently, Elliott Sprehn turned me onto a Java class called a TreeMap. The TreeMap implements the Map interface; but, it can iterate over the collection of keys using a composed comparator. By default, the comparator sorts the keys by alphabetical order. If we can find a way to make sure that our array indicies are alphabetically "correct," we should be able to use a TreeMap in order to create a collection-based array.

  • <cffunction
  • name="arrayCollection"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I return a the given array as a collection of array keys in natural order. In order to maintain proper numeric ordering, the keys are zero-padded to all be the same length.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="array"
  • type="array"
  • required="true"
  • hint="I am the array for which we are getting the key collection."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var local = {} />
  •  
  • <!---
  • Create our key collection. By using Java's TreeMap, we
  • will provide struct-like behavior in which the key iterator
  • returns keys in a natural order - alphabetically as strings.
  • We will need to prefix the values to make sure the return
  • in the correct order.
  • --->
  • <cfset local.keys = createObject( "java", "java.util.TreeMap" ).init() />
  •  
  • <!---
  • Beacuse the default sorting of the TreeMap is alphetical, it
  • will cause a problem for our numeric keys. We *could* write
  • a numeric comparator in Java - but, for this demo, we will
  • just be zero-padding to normalize the alpha-numeric gap.
  • Calculate the zero-padding that we need to supply for the
  • index as we add it to the map.
  • --->
  • <cfset local.zeroPadding = repeatString(
  • "0",
  • len( arrayLen( arguments.array ) )
  • ) />
  •  
  • <!---
  • Get the character width we want to limit the key length to.
  • This is to help us order them in natural order.
  • --->
  • <cfset local.keyLength = len( local.zeroPadding ) />
  •  
  • <!---
  • Loop over the arrays indicies to add them to the key map as
  • index-value pairs.
  • --->
  • <cfloop
  • index="local.index"
  • from="1"
  • to="#arrayLen( arguments.array )#"
  • step="1">
  •  
  • <!--- Check to see if the current index is defined. --->
  • <cfif arrayIsDefined( arguments.array, local.index )>
  •  
  • <!---
  • Add the index to the map. As we do this, we are going
  • to left-zero-pad the index values to make the all
  • strings of the same order.
  • --->
  • <cfset local.keys.put(
  • javaCast(
  • "string",
  • right(
  • (local.zeroPadding & local.index ),
  • local.keyLength
  • )
  • ),
  • arguments.array[ local.index ]
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • <!--- Return the array collection. --->
  • <cfreturn local.keys />
  • </cffunction>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Create an array of values. Make sure to go more than 10 to
  • ensure that keys are sorting correctly.
  • --->
  • <cfset women = [
  • "Sarah",
  • "Jill",
  • "Katie",
  • "Joanna",
  • "Kim",
  • "Tricia",
  • "Kit",
  • "Samantha",
  • "Nancy",
  • "Michelle",
  • "Natalie",
  • "Allison"
  • ] />
  •  
  • <!---
  • Skip a few indicies to make sure the conversion can properly
  • handle empty index values.
  • --->
  • <cfset women[ 20 ] = "Jo" />
  • <cfset women[ 25 ] = "Kristen" />
  •  
  •  
  • <cfoutput>
  •  
  • <!---
  • Loop over the array using the keys and an item collection.
  • Notice that the even though the keys are left-zero-padded,
  • ColdFusion will have no problem treating them as numeric
  • indexes when referencing the array.
  • --->
  • <cfloop
  • item="index"
  • collection="#arrayCollection( women )#">
  •  
  • #index#: #women[ index ]#<br />
  •  
  • </cfloop>
  •  
  • </cfoutput>

As you can see in this demo, we are using a collection loop to iterate over an array. But, we aren't iterating over the array directly; rather, we're passing the array to our arrayCollection() user-defined function (UDF), which converts our array into a TreeMap. When we run the above code, we get the following output:

01: Sarah
02: Jill
03: Katie
04: Joanna
05: Kim
06: Tricia
07: Kit
08: Samantha
09: Nancy
10: Michelle
11: Natalie
12: Allison
20: Jo
25: Kristen

This iterates over the collection in index-order, using only the indicies that reference defined values.

This works, but it's not quite awesome. Because the default comparator of the TreeMap uses alphabetical comparisons, we have to zero-pad our index keys. If we don't do this, then alphabetically, "10" comes right after "1." Sure, we could have dropped down into the Java layer to build a numeric comparator; but, that would just have made the solution all the more complex.

Typically with a struct, we don't have control over the order in which are keys get assigned. In this case, however, since the conversion from array to struct is encapsulated within our UDF, insertion order is something that we do have control over. This scenario allows us to use a different kind of "ordered struct" - the LinkedHashMap.

The LinkedHashMap is another implementation of the Map interface. Unlike the TreeMap, however, the LinkedHashMap doesn't use key comparators. Rather, the LinkedHashMap iterates over the keys of the collection in insertion-order. That is, it returns the keys in the same order in which they were defined. This allows us to form the same kind of conversion with less code and friendlier keys:

  • <cffunction
  • name="arrayCollection"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I return a the given array as a collection of array keys in insertion order.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="array"
  • type="array"
  • required="true"
  • hint="I am the array for which we are getting the key collection."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var local = {} />
  •  
  • <!---
  • Create our key collection. By using Java's LinkedHashMap, we
  • will provide struct-like behavior in which the key iterator
  • returns keys in an insertion order. And, since we are in
  • control of the inserting, that order will be in index order.
  • --->
  • <cfset local.keys = createObject( "java", "java.util.LinkedHashMap" ).init() />
  •  
  • <!---
  • Loop over the arrays indicies to add them to the key map as
  • index-value pairs.
  • --->
  • <cfloop
  • index="local.index"
  • from="1"
  • to="#arrayLen( arguments.array )#"
  • step="1">
  •  
  • <!--- Check to see if the current index is defined. --->
  • <cfif arrayIsDefined( arguments.array, local.index )>
  •  
  • <!---
  • Add the index to the map. Since the keys are returned
  • in insertion order, we don't have to worry about the
  • format of the keys.
  • --->
  • <cfset local.keys.put(
  • javaCast( "string", local.index ),
  • arguments.array[ local.index ]
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • <!--- Return the array collection. --->
  • <cfreturn local.keys />
  • </cffunction>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <cfoutput>
  •  
  • <!---
  • Loop over the array using the keys and an item collection.
  • Since our linked hash map returns the keys in insertion
  • order, our collection will iterate in index order.
  • --->
  • <cfloop
  • item="index"
  • collection="#arrayCollection( women )#">
  •  
  • #index#: #women[ index ]#<br />
  •  
  • </cfloop>
  •  
  • </cfoutput>

As you can see, this code was much more straightforward - no zero-padding, no key manipulation. We're simply adding the keys in an order reflective of the given array. And, when we run this code, we get the following output:

1: Sarah
2: Jill
3: Katie
4: Joanna
5: Kim
6: Tricia
7: Kit
8: Samantha
9: Nancy
10: Michelle
11: Natalie
12: Allison
20: Jo
25: Kristen

Works like a charm. And, the keys look more like pure numeric values.

It's not often that I ever care about the order in which the keys of a struct are returned. And, it's even less often that I have arrays with undefined values. But, in the double-rare situation in which both those cases are true, we can dip down into the Java layer and use the TreeMap or the LinkedHashMap to help convert arrays to structs that can be treated as ordered collections.




Reader Comments

Dec 7, 2010 at 10:40 AM // reply »
38 Comments

You don't have to use numeric keys, but be aware that all keys are case-specific when using "LinkedHashMap" (unlike a normal struct.)

(I tried posting this response in Firefox 3.6.12 Windows and it didn't work... not sure why. Reposted using Google Chrome.)


Dec 7, 2010 at 10:43 AM // reply »
10,743 Comments

@James,

Good point about the Java-based maps and case-sensitivity; I haven't tested that myself, but I believe I have seen that mentioned on some other blog. Luckily, in this case, numbers don't have a "case".

Funky re: posting. I am not sure why a different browser would work. Probably blog just hiccuped for a second (I only use FireFox to interact with my blog).


Dec 7, 2010 at 2:04 PM // reply »
1 Comments

Actually I can tell you that I prefer using the

  • <cfloop collection="#somearray#" item="index">
  • current element index: #index#
  • </cfloop>

in Railo more and more since it not only saves me some typing, but I can always use <cfloop collection> and I don't have to worry what is it again? index or item? does it contain the key or the element? Anyway, the double benefit that only existing keys are respected makes it even more valuable for me.
And I must admit that I always disliked the fact that I have to pass in arrayLen(somearray) to my to="" attribute. I hate it when I have to evaluate additional functions just to get the end of a loop. I even saved the array lenght in a variable before the loop and then looped over up to this variable, just that I don't have to execute the arrayLen() function.
The only downside of your approach is that ACF uses reflection (just as Railo 3.x would do) in order to do all the Java calls. Which is quite time consuming. But I am sure you have some execution times there as well.

Gert


Dec 7, 2010 at 2:14 PM // reply »
10,743 Comments

@Gert,

I definitely like the fact that you don't have to know what kind of item you're actually dealing with struct or array. It's like a generic each iterator that just worries about a given interface, not an actual data type.


Dec 9, 2010 at 6:28 AM // reply »
18 Comments

Awesome example and explanation, and few nice comments with good points.

Cheers Mate, you rock.
:)


Feb 8, 2011 at 6:07 PM // reply »
22 Comments

I wrote my own version of your arrayCollection() functions using TreeMaps and LinkedHashMaps without reading yours based on your sample output. Turns out we think alike!

Main differences are you used arrayIsDefined() where I used isNull() with Array.get() and I favoured cfscript.

The TreeMap version is heaps less efficient with all of the key padding. Since we're talking about looping over arrays, the index will always be sorted, so a LinkedHashMap makes a lot more sense. It also seems a bit hacky asking for #women[01]# to get the first element.

  • function arrayCollection( Array arr ) {
  • local.treeMap = createObject( 'java', 'java.util.TreeMap' ).init();
  • local.size = arr.size();
  • local.chars = len( local.size );
  • local.pad = repeatString( '0', local.chars );
  • for ( local.i = 0; local.i lt local.size; local.i++ ) {
  • if ( ! isNull( arr.get( local.i ) ) ) {
  • local.key = right( local.pad & local.i + 1, local.chars );
  • local.treeMap.put( local.key, arr.get( local.i ) );
  • }
  • }
  • return local.treeMap;
  • }
  • function arrayCollection2( Array arr ) {
  • local.hashMap = createObject( 'java', 'java.util.LinkedHashMap' ).init();
  • local.size = arr.size();
  • for ( local.i = 0; local.i lt local.size; local.i++ ) {
  • if ( ! isNull( arr.get( local.i ) ) ) {
  • local.key = local.i + 1;
  • local.hashMap.put( local.key, arr.get( local.i ) );
  • }
  • }
  • return local.hashMap;
  • }


Oct 6, 2011 at 3:30 PM // reply »
1 Comments

Another post that saved me a ton of time with my project! Thanks for probing the obtuse bits of ColdFusion for us and bringing back these gems. I owe you a few beers at this point...


Apr 13, 2012 at 11:29 AM // reply »
2 Comments

Hi everybody.

Using java.util.LinkedHashMap to keep ordering is nice. But how can i remove an element of that struct? There is no delete or remove methods and StructDelete() give me errors... (coldfusion 8)

Thanx alot.


Apr 18, 2012 at 12:46 AM // reply »
2 Comments

My mistake. I was trying to delete an element of the java.util.LinkedHashMap in a for loop (decrement). Its possible, in other languages, to delete properties of an object that way but not in coldfusion apparently...

Anyway, thank you for your website, great ressources for a coldfusion noob like me ;)


Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 16, 2012 at 8:18 PM
Best Of ColdFusion 10 Contest Entry - HTML Email Utility
Just found this, looks good! I'm trying to run it on local, it's the 64bit version and I'm experiencing horrible lag. On average the generate.cfm processes the content change in 60-90 seconds. I've ... read »
May 16, 2012 at 6:40 PM
Maintaining Sessions Across Multiple ColdFusion CFHttp Requests
I am trying to integrate this CFHTTPsession into an application that will log into zeekrewards.com to post ads and I am not having any luck. The code works perfectly for logging into other websites, ... read »
May 16, 2012 at 2:44 PM
Creating A Sometimes-Fixed-Position Element With jQuery
Thank you, very useful technique! Worked like a charm. ... read »
May 16, 2012 at 1:58 PM
Movies As A Religious Experience
Acting can, in a way, ruin the movie-goer's experience. I used to be able to get so caught up in movies and their plots, and totally engaged. But lately, I haven't been able to as much with a lot o ... read »
May 16, 2012 at 1:52 PM
The Science Of Optimal Post-Exercise Nutrition
children of this age eat very less vegetables so u can opt for salads they will like it also carrot ,cucumber,onion and as far as pulses are concerned u can boil them ,give him along with mashed rice ... read »
May 16, 2012 at 1:34 PM
Strange ColdFusion JRUN Stack Overflow Error
Hey, Recently I updated my jrun4 using the latest updater 7 and now i am having memory issues :(:(:( any help is appreciated ... read »
May 16, 2012 at 9:56 AM
ColdFusion 10 Beta, Apache Tomcat, And Symbolic Links On Mac OSX
Hi, Now that ColdFusion 10 is out I have stumbled over this as well and I cannot figure out the proper solution. We're running virtual hosts via Apache2; the ColdFusion-applications store their fil ... read »
May 15, 2012 at 6:03 PM
Movies As A Religious Experience
@Ben, I don't know whether you'd consider this a religious observation, but it seems to me, in a sense, movies multiply how many lives we get to have. Each movie is like a little extra life we get ... read »