Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Michiel Westerbeek
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Michiel Westerbeek

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

By
Published in Comments (9)

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.

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

Reader Comments

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

15,848 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).

2 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

15,848 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.

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

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.

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

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