Following up on my series of blog entries from last week, here is another gotcha you want to look out for when using ColdFusion arrays. When defining an array in ColdFusion, you typically set them starting with 1 and ending with some number:
<cfset arr = arrayNew(1)>
<cfloop index="x" from="1" to="10">
<cfset arr[x] = randRange(1,100)>
</cfloop>
This creates an array with items in indexes 1 to 100. But - consider this example:
<cfset arr = arrayNew(1)>
<cfloop index="x" from="1" to="10">
<cfset arr[randRange(1,100)] = x>
</cfloop>
Now this is a bit convoluted, but this code example assigns values based on a random index. It will create 10 array values in indexes from 1 to 100 (and it may even do the same index twice).
What happens if you try to loop over this array? The normal way to loop over an array is to use arrayLen:
<cfloop index="x" from="1" to="#arrayLen(arr)#">
If you run this code on the array we just created, you will get an error because the indexes aren't all defined. What about isDefined()? It won't work on arrays. So what do you do?
One solution is to try/catch:
<cfloop index="x" from="1" to="#arrayLen(data)#">
<cftry>
<cfoutput>Item #x# is #data[x]#<br /></cfoutput>
<cfcatch></cfcatch>
</cftry>
</cfloop>
Another option is to use a UDF that wraps up the same logic. You can find arrayDefinedAt() at CFLib. Here is an example of code that uses it:
<cfloop index="x" from="1" to="#arrayLen(data)#">
<cfif arrayDefinedAt(data,x)>
<cfoutput>Item #x# is #data[x]#<br /></cfoutput>
</cfif>
</cfloop>
Lastly, you can just wait for Scorpio! One of the announced features is an arrayiIsDefined function. Ok, most likely you can't wait for Scorpio, but it's nice to know Adobe took care of this problem.
Archived Comments
Why on earth would you want to do this?
Have an array with missing indexes? I don't know. But it happens. Not saying it is a GOOD thing. ;)
Is there a way for loop over the array with an iterator-type syntax?
Something along the lines of...
<cfloop index="x" from="data.first()" to="#data.end()#" step="#data.next()#">
...
</cfloop>
OR
<cfloop index="x" array="#data#">
...
</cfloop>
Am I just thinking wishfully here?
>Why on earth would you want to do this?
Here's an example I've run into: An array of error codes and their text equivalents:
arrErrors[10] = "Unexpected Result from the database";
arrErrors[13] = "Improperly formed parameter";
arrErrors[14] = "Action not allowed";
Say you wanted error #10 to be "Unexpected Result from the database" across your entire app, but errors 1-9 aren't applicable to the object you're coding. So you call an error handler with setError(10), which looks up the right error in the arrErrors array.
You could code it as a struct, but an array is a faster lookup.
Sorry, that should be an error thrower, not an error handler.
Cheers Andrew, funny how this prob came up for me just after I read this post.
The iterator works a treat, cheers for the pointers.
<cfset test = arrayNew(1)/>
<cfset test[1] = "one"/>
<cfset test[3] = "three"/>
<cfset test[5] = "five"/>
<cfset testIterator = test.iterator()/>
<cfloop condition="testIterator.hasNext() eq true">
<cfoutput>#testIterator.next()#</cfoutput>
</cfloop>
I ran into this before and what I did was use variable variables to create a pseudo-Array and then checked for their existence. There could be anywhere between 1 and 5 results. Probably not the most elegant solution but it was how I got around the isDefined() problem at the time.
<cfquery name="Get_RFP" datasource="#application.DSN#" >
SELECT tblRPEmp.RespID, tblRP.ID, tblRP.CtrlNum
FROM tblRP Etc Etc
</cfquery>
<!--- See if there are more than one RespID / CtrlNum--->
<cfset tblRP_CtrlNum_Valuelist = ValueList(Get_RFP.CtrlNum) >
<!--- Find out who is the Responsible partner --->
<cfoutput query="Get_RFP">
<cfset "RspNames_#CtrlNum#_#RespID#" = #LName#>
</cfoutput>
<!--- See how many CtrlNum's there are --->
<cfset TMPListValueCount = ListValueCount(tblRP_CtrlNum_Valuelist, CtrlNum)>
<!--- if more than one --->
<cfif TMPListValueCount NEQ 1>
<!--- See if they exist and then Spit out who the Responsible partners --->
<cfLoop Index="L" FRom="2" To="5">
<cfif IsDefined("RspNames_#CtrlNum#_#L#")> #Evaluate("RspNames_#CtrlNum#_#L#")# - #L# <br /> </CFIF>
</CFLOOP>
</CFIF>
You're welcome dc :)
@dc- Is the iterator method part of Java?
Array with missing index isn't that special. It's quite often nice to load data from some related tables to memory indexed by primary keys.
I did some metrics testing on access times for arrays and structures and sparse arrays and sparse structures. Anyone using sparse arrays might want to reconsider:
http://rickosborne.org/blog...
@dc
This syntax works as well.
<cfset elements = arr.elements() />
<cfloop condition="#elements.hasMoreElements()#">
#elements.nextElement()#<br />
</cfloop>
Keep in mind that both of these return empty strings for undefined array elements.
@Tom - Yes, dropping out into Java and the java.util.Vector object
@jason - that seems a little more elegant, might go and revisit my code again, cheers :)
Hey Ray, maybe you already knew this, but for those coming along later, the RC changed arraydefinedat to arrayisdefined (which actually is named more like the older isdefined). Just an FYI.
Nope, I had missed that. Thanks Charlie.