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 CFUNITED 2010 (Landsdown, VA) with:

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

By Ben Nadel on
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.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

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

Reply to this Comment

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

Reply to this Comment

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

Reply to this Comment

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

Reply to this Comment

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;
  • }

Reply to this Comment

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

Reply to this Comment

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.

Reply to this Comment

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

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.