Thursday, 30 August 2012

Arrays in CFML (part 4)

G'day:
As the title suggests, this is part four of a series of articles I've written about arrays in CFML. So far I've covered what an array is and how to create them, plus two articles covering all the functions in CFML which specifically work with arrays.
As well as these functions that are specifically implemented to maintain & manipulate arrays, there are a bunch of other functions relating to other data types that use arrays. I'm going to cover those in this article. I'll break them down by the data-type they relate to.  I'm not going to do indepth coverage of how the functions work, because I'll eventually cover them when I give the various other data types the same treatment I'm giving arrays here.  EG: I'll discuss structFindKey() thoroughly when I do an exploration into "Structs in ColdFusion" (OK, so you've been fore-warned... there's more of this to come! ;-).  I'm just gonna dwell on the array`ed-ness of the relevant functionality.



Strings

A few string-manipulation functions return arrays.

REFind() / REFindNoCase()

Regexes are a topic into themselves (which I will cover at some point, but in the mean time you could read what Ben Nadel has to say on the matter), so I'll not go too much into them here, other then use some jargon. When doing a regex find, one can capture subexpressions as well as the main match. By default REFind() discards these subexpression matches and just returns the index of the starting point of the main match. However the fourth argument of REFind() allows one to return the subexpressions too. 
REFind(reg_expression, string [, start, returnsubexpressions ] )

When this argument is true, REFind() returns a struct instead of an integer, and the struct has keys pos and len, each of which are arrays wherein each element represents the position and length of a subexpression match (the first array element is the "main" match).  EG:

regex = '<cffunction[^>]+returntype="(struct|array)"[^>]*>';
sample = '<cffunction name="getObjects" returntype="struct" access="public" output="false" hint="Returns the objects as a struct, keyed on ID.">';

matchResult = reFind(regex, sample, 1, true);

writeDump(matchResult);

functionSignature    = mid(sample, matchResult.pos[1], matchResult.len[1]);
returnType            = mid(sample, matchResult.pos[2], matchResult.len[2]);

writeOutput("Function signature: #htmlEditFormat(functionSignature)#<
");
writeOutput("Return type: #returnType#<
");

Result:
struct
LEN
array
1134
26
POS
array
11
243


Function signature: <cffunction name="getObjects" returntype="struct" access="public" output="false" hint="Returns the objects as a struct, keyed on ID.">
Return type: struct


I think the structure of the return type here is quite odd.  To me, it should be an array of structs, not a struct of arrays, eg:

array
1
struct
LEN134
POS1
2
struct
LEN6
POS43

One doesn't work with all the lengths, and then all the positions separately, one works with each match's pos & len.  Still: that's a minor gripe.  It's handy functionality though, that's for sure.

Obviously reFindNoCase() works the same as reFind().

REMatch() / REMatchNoCase()


Like reFind(), reMatch() does a regex find in a string, the difference being that it returns all matches found, not just the first one.  On the other hand, it only returns the main match, and does not have the capability to return all the subexpression info as well.  I think Adobe dropped the ball here.  Returning all matches is a great idea... I almost always want all of them, not just the first one, but also I almost always want the subexpressions too.  So we've got two functions (well: four, with the ~nocase versions), neither of which cover all the functionality.  What Adobe should have done is just added a scope argument to reFind(), thus:

REFind(reg_expression, string [, start, returnsubexpressions, scope ] )
Where the scope argument works the same as per replace() (etc), in that it specifies either "ONE" or "ALL".  That would have been the sensible thing to do.  I'd even go as far as to say that reMatch() / reMatchNoCase() should be deprecated in favour of this. I've raised an E/R for this: 3321666.

Anyway, we have them for the time being, so I'll clam-up about that and show you the array stuff.  Both these functions returns an array of matches:

regex = '"[^"]+"';
sample = '<cffunction name="getObjects" returntype="struct" access="public" output="false" hint="Returns the objects as a struct, keyed on ID.">';

matchResult = reMatch(regex, sample);

writeDump(matchResult);

array
1"getObjects"
2"struct"
3"public"
4"false"
5"Returns the objects as a struct, keyed on ID."

We've got a list of attribute values.  Here's a case in point of where the subexpressions would be handy... I want to capture the stuff between quotes, but I don't actually want the quotes.  So it'd be great if reMatch() would respect subexpressions so I could match the bare values separately.

Lists 


listToArray()

The only list function that interacts with an array (other than arrayToList() which I've already covered) is listToArray(). It's self-explanatory what this does: it takes a list and converts it to an array. Prior to ColdFusion implementing shorthand notation for creating arrays, this was the easiest way to get simple data into an array:

numbers = listToArray("tahi,rua,toru,wha,rima,ono,whitu,waru,iwa,tekau");
writeDump(numbers);

oddNumbers = listToArray("tahi,,toru,,rima,,whitu,,iwa,", ",", true);
writeDump(oddNumbers);

array
1tahi
2rua
3toru
4wha
5rima
6ono
7whitu
8waru
9iwa
10tekau
array
1tahi
2[empty string]
3toru
4[empty string]
5rima
6[empty string]
7whitu
8[empty string]
9iwa
10[empty string]

As demonstrated: this approach still had one benefit over short-hand notation: one can create a sparse array.  It's not easy to achieve this with shorthand notation, because trying to do this:

evenNumbers = [, "rua", , "wha", , "ono", , "waru",  , "tekau"];
writeDump(evenNumbers);

Yields a compiler error:

The following information is meant for the website developer for debugging purposes.
Error Occurred While Processing Request

Invalid CFML construct found on line 2 at column 15.

ColdFusion was looking at the following text:[

Creating a sparse array is possible with shorthand notation, but it's clumsy:

evenNumbers = [javaCast("null", ""), "rua", javaCast("null", ""), "wha", javaCast("null", ""), "ono", javaCast("null", ""), "waru",  javaCast("null", ""), "tekau"];
writeDump(evenNumbers);

(One might think one could tidy that up by doing this):

null = javaCast("null", "");
evenNumbers = [null, "rua", null, "wha", null, "ono", null, "waru",  null, "tekau"];

But because of CF's inability to cope with NULL in CFML, this errors with this:

The following information is meant for the website developer for debugging purposes.
Error Occurred While Processing Request

Variable NULL is undefined.


I wish Adobe would just get on with it and add proper support for the concept of NULL in CFML (note: there is 3042895 to cover this, but it's closed "never fix".  Nice).


Note that arraySort() is listed in the docs as a list function, but other than the fact the example code uses a list function, it's got nothing to do with lists.

Structs


structFindKey() / structFindValue()

Both of these look-up data from a struct, and return the results in an array.  EG:

numbers = {
    maori    = {
        one        = "tahi",
        two        = "rua",
        three    = "toru",
        four    = "wha"
    },
    japanese= {
        one        = "ichi",
        two        = "ni",
        three    = "san",
        four    = "shi"
    }
};
ones = structFindKey(numbers, "one", "all");
rua = structFindValue(numbers, "rua", "all");

writeDump(var=numbers, label="data");
writeDump(var=ones, label="Struct keys that are 'one'");
writeDump(var=rua, label="struct elements with a value of 'rua'");


data - struct
JAPANESE
data - struct
FOURshi
ONEichi
THREEsan
TWOni
MAORI
data - struct
FOURwha
ONEtahi
THREEtoru
TWOrua
Struct keys that are 'one' - array
1
Struct keys that are 'one' - struct
owner
Struct keys that are 'one' - struct
FOURwha
ONEtahi
THREEtoru
TWOrua
path.MAORI.one
valuetahi
2
Struct keys that are 'one' - struct
owner
Struct keys that are 'one' - struct
FOURshi
ONEichi
THREEsan
TWOni
path.JAPANESE.one
valueichi
struct elements with a value of 'rua' - array
1
struct elements with a value of 'rua' - struct
keyTWO
owner
struct elements with a value of 'rua' - struct
FOURwha
ONEtahi
THREEtoru
TWOrua
path.MAORI.TWO


The ins and outs of these functions are outwith the remit of this article, and to be frank the link to arrays is tenuous other than the return values, but they're handy and under-utilised functions, so I figured I'd include them.  One good thing is that the owner key in the return result is a reference to the owner struct, not simply a copy of its value, so one can manipulate the underlying struct via the returned array.  Handy.

structKeyArray()

structKeyArray() extracts the keys from a struct, representing them as an array. Structs don't store their keys in a guaranteed order, but sometimes one needs to access a struct's values according to some sense of key order (rather then the order of the values, for which one would use structSort()). structKeyArray() facilitates this. One can pull the keys out with structKeyArray(), sort the array, then use it as a key lookup. EG:

letters = {
    e    = "echo",
    a    = "alpha",
    b    = "bravo",
    d    = "delta",
    c    = "charlie"
};
writeDump(var=letters, label="Under the hood, &lt;cfdump&gt; sorts them");
writeOutput("<hr />");

writeOutput("Struct keys not sorted by default:<
");
for (letter in letters){
    writeOutput("#letter#: #letters[letter]#<
");
}
writeOutput("<hr />");

writeOutput("structKeyArray() not sorted by default:<
");
keys = structKeyArray(letters);
writeDump(keys);
writeOutput("<hr />");

writeOutput("Having sorted the array, we can access the struct keys in alphabetic order:<
");
arraySort(keys, "textnocase");
writeDump(keys);

It's important to remember that just as the keys in the struct don't have a guaranteed order, nor do the keys in the array returned from structKeyArray():
 
Under the hood, <cfdump> sorts them - struct
Aalpha
Bbravo
Ccharlie
Ddelta
Eecho

Struct keys not sorted by default:
D: delta
E: echo
A: alpha
B: bravo
C: charlie

structKeyArray() not sorted by default:
array
1D
2E
3A
4B
5C

Having sorted the array, we can access the struct keys in alphabetic order:
array
1A
2B
3C
4D
5E

structSort()

If one needs to sort a struct by its values, one uses structSort(). As mentioned above, structs don't (and can't) store their keys with a guaranteed sense of order, so when doing a structSort(), the struct itself isn't sorted or modified in any way. What happens with structSort() is that it returns the keys in the sequence of the ordered values via an array: because obviously an array does have a sense if ordering. So much like structKeyArray(), one does a structSort(), then loops over the ordered array to access the struct in that order:

numbers = {
    five    = "rima",
    four    = "wha",
    three    = "toru",
    two        = "rua",
    one        = "tahi",
    six        = "ono"
};

writeOutput("Struct keys/values not sorted by default:<
");
writeDump(structKeyArray(numbers));
writeOutput("<hr />");
writeOutput("structSort() returns an array of keys according to the ordering of the key values:<
");
keys = structSort(numbers);
for (number in keys){
    writeOutput("#number#: <strong>#numbers[number]#</strong><
");
}
writeOutput("(It's a bit odd to sort numbers alphabetically by their name, but it demonstrates the point!)");

Struct keys/values not sorted by default:
array
1ONE
2TWO
3THREE
4FOUR
5SIX
6FIVE

structSort() returns an array of keys according to the ordering of the key values:
SIX: ono
FIVE: rima
TWO: rua
ONE: tahi
THREE: toru
FOUR: wha
(It's a bit odd to sort numbers alphabetically by their name, but it demonstrates the point!)

XML

Within an XML document, the sequence of nodes within a given parent node have a sense of order. Whilst this node collection is not a native ColdFusion array (see below), it can be accessed via array notation, and some array functions can be used to manipulate the nodes. This will be covered in more detail when I examine ColdFusion's support for XML, but here's a demonstration of working with an XML node set as if it's an array:

<cfxml variable="x">
    <aaa>
        <bbb>ccc</bbb>
        <ddd eee="fff">ggg</ddd>
        <hhh />
    </aaa>
</cfxml>
<cfset children = x.aaa.xmlChildren>
<cfoutput>
    Data type of xmlChildren: #children.getClass().getName()#<

    But can access them as an array:<

    <cfdump var="#children[2]#" label="Second child"><

    And use array functions:<

    Number of children: #arrayLen(children)#<

    Delete the second child using arrayDeleteAt():<

    <cfset arrayDeleteAt(children, 2)><!--- ddd --->
    <cfdump var="#x#" label="&lt;ddd&gt; is gone">
</cfoutput>


Data type of xmlChildren: coldfusion.xml.XmlNodeArray
But can access them as an array:
Second child - xml element
XmlNameddd
XmlNsPrefix
XmlNsURI
XmlTextggg
XmlComment
XmlAttributes
Second child - struct
eeefff
XmlChildren

And use array functions:
Number of children: 3
Delete the second child using arrayDeleteAt():
<ddd> is gone - xml document [short version]
aaa
XmlText
bbb
XmlTextccc
hhh
XmlText

xmlSearch()


Beyond accessing node sets via array notation, xmlSearch() preserves the ordering - as they appear in the XML doc relative to each other - via an array:

<cfxml variable="x">
    <aaa>
        <bbb>ccc</bbb>
        <ddd eee="fff">ggg</ddd>
        <hhh />
        <iii>
            <hhh jjj="kkk">lll</hhh>
        </iii>
    </aaa>
</cfxml>
Initial state of XML:<

<cfdump var="#x#">
<hr />
&lt;hhh&gt; nodes:<

<cfset hhh = xmlSearch(x, "//hhh")>
<cfdump var="#hhh#">
<hr />

Adjust one of the nodes via the array, not the XML doc<

<cfset hhh[1].xmlText = "mmm">
Note that the previously empty &lt;hhh/&gt; node now has xmlText:<

<cfdump var="#x#">

Initial state of XML:
xml document [short version]
aaa
XmlText
bbb
XmlTextccc
ddd
XmlTextggg
XmlAttributes
struct
eeefff
hhh
XmlText
iii
XmlText
hhh
XmlTextlll
XmlAttributes
struct
jjjkkk

<hhh> nodes:
array
1
xml element
XmlNamehhh
XmlNsPrefix
XmlNsURI
XmlText
XmlComment
XmlAttributes
struct [empty]
XmlChildren
2
xml element
XmlNamehhh
XmlNsPrefix
XmlNsURI
XmlTextlll
XmlComment
XmlAttributes
struct
jjjkkk
XmlChildren

Adjust one of the nodes via the array, not the XML doc
Note that the previously empty <hhh/> node now has xmlText:
xml document [short version]
aaa
XmlText
bbb
XmlTextccc
ddd
XmlTextggg
XmlAttributes
struct
eeefff
hhh
XmlTextmmm
iii
XmlText
hhh
XmlTextlll
XmlAttributes
struct
jjjkkk

One thing to recall here (as demonstrated above) is that this is an array of references to the given nodes, not simply their values, so one can modify the XML directly via the start elements.

ColdFusion ORM

Many of the ORM function suite return arrays of objects. This topic is too big to cover under a paragraph in an article about arrays, and I'll write dedicated articles on the topic of ORM at some stage in the future.

Misc

There s couple of odds 'n' sods that I can't think of a grouping for, so I'll just bung them down here.

directoryList()

Rather than being a direct analogue of <cfdirectory action="list"> (which is what would have made sense), directoryList()'s default behaviour is to return an array of file paths.

files = directoryList(expandPath("./"));
writeDump(var=files, top=5);

array
1C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays\arrayAppend.cfm
2C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays\arrayClear.cfm
3C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays\arrayContains.cfm
4C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays\arrayDelete.cfm
5C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays\arrayDeleteAt.cfm

I think it should've just been implemented the same as <cfdirectory>, but however.

query [Filtered - Top 5 of 47 rows]
ATTRIBUTESDATELASTMODIFIEDDIRECTORYMODENAMESIZETYPE
1[empty string]16/08/12 21:19C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays[empty string]arrayAppend.cfm387File
2[empty string]16/08/12 22:37C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays[empty string]arrayClear.cfm108File
3[empty string]22/08/12 00:32C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays[empty string]arrayContains.cfm3244File
4[empty string]23/08/12 22:27C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays[empty string]arrayDelete.cfm4340File
5[empty string]16/08/12 22:35C:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays[empty string]arrayDeleteAt.cfm341File

(and, yes, I know one can still get a query from directoryList(), but I don't see why they created a function-version of <cfdirectory> so as to expose the functionality of it ito CFScript, and then implement it to behave differently. Daft.  And, as I am becoming increasingly wont to say: remoinds me of PHP (equivalent things behaving differently is just messy and incoherent)).


CallStackGet() 

Despite being named by Yoda, this is a handy new function in CF10.  It dumps where the current line of code is in the sequence of files being processed, plus all the points in its ancestors where processing changed location (via an include, or a function call, etc).  And it returns all this in an array:

<!--callStackGet.cfm --->
<cfinclude template="./incCallStackGet.cfm">


<cfdump var="#getCallStack()#">


<!--- incCallStackGet.cfm --->

<cfscript>
    function getCallStack(){ // I'm not Yoda
        return callStackGet();
    }
</cfscript>

array
1
struct
FunctionGETCALLSTACK
LineNumber5
TemplateC:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays\incCallStackGet.cfm
2
struct
Function[empty string]
LineNumber5
TemplateC:\ColdFusion10\cfusion\wwwroot\shared\git\blogExamples\arrays\callStackGet.cfm

Note: this is supported in Railo, but not in OpenBD.

Query columns

Query columns can be treated as arrays!  This is quite handy.  One needs to use associative array notation, eg: query["columnName"], and if one uses it in a place in which a string could be used, it'll simply act like the first element of the column, not the whole column.  But if one uses it in a situation that calls for an array, CF will cast it to an array. EG:

data = queryNew(
    "id,english,maori",
    "integer,varchar,varchar", 
    [
        [1, "one", "tahi"],
        [2, "two", "rua"],
        [3, "three", "toru"],
        [4, "four", "wha"]
    ]
);

writeOutput("Data type of a query column: " & data["english"].getClass().getName() & "<
");

writeDump(data);
writeOutput("isArray(): " & isArray(data["english"]) & "
");

writeOutput('Because writeDump() can take a string, CF will interpret data["english"] as a string in this situation: ');
writeDump(data["english"]);
writeOutput("<hr />");
writeOutput("Use arrayEach() to iterate over a query column:<
");
arrayEach(
    data["english"],
    function(element){
        writeOutput(element & "<
");
    }
);

Data type of a query column: coldfusion.sql.QueryColumn
query
ENGLISHIDMAORI
1one1tahi
2two2rua
3three3toru
4four4wha
isArray(): YES
Because writeDump() can take a string, CF will interpret data["english"] as a string in this situation: one

Use arrayEach() to iterate over a query column:
one
two
three
four

This doesn't work on Railo as it doesn't consider a column to be castable to an array.  And this example code wouldn't run at all on OpenBD, so I dunno what it does.  OpenBD supports neither the inline-data syntax of queryNew(), nor arrayEach(). I couldn't be bothered rewriting it to find out what OpenBD does.

On a whim I thought I'd have a look at whether I could concatenate an array onto a query column:

query = queryNew(
    "id,english,maori",
    "integer,varchar,varchar", 
    [
        [1, "one", "tahi"],
        [2, "two", "rua"],
        [3, "three", "toru"],
        [4, "four", "wha"]
    ]
);

array = ["rima","ono","whitu","waru","iwa","tekau"];

arrayAppend(query["maori"], array, true);
writeDump(variables);

Err.... no.  ColdFusion gave me this rather odd-sounding error message:

The following information is meant for the website developer for debugging purposes.
Error Occurred While Processing Request

The '' method cannot be applied to a query column.

Query columns do not support methods that would alter the structure of a query column; you must use an analogous method on the query.

Nice.  The gist being: "that won't work".  I didn't expect it to.


Arguments scope

The Arguments scope is a bit of a curious one.  It behaves rather like a struct, but it also behaves rather like an array.  Whilst being neither, and not really know what it itself is.  Here's what I mean:

void function f(){
    writeDump(arguments);
    writeOutput("isArray(): #isArray(arguments)#<
");
    writeOutput("isStruct(): #isStruct(arguments)#<
");
    writeOutput("arguments.getClass().getName: #arguments.getClass().getName()#<
");
    writeOutput("arrayLen(): #arrayLen(arguments)#<
");
    writeOutput("structCount(): #structCount(arguments)#<
");
}

f("tahi", "rua", "toru", "wha");
writeOutput("<hr />");

f(one="tahi", two="rua", three="toru", four="wha");
writeOutput("<hr />");

struct
1tahi
2rua
3toru
4wha
isArray(): NO
isStruct(): YES
arguments.getClass().getName: coldfusion.runtime.ArgumentCollection
arrayLen(): 4
structCount(): 4

struct
fourwha
onetahi
threetoru
tworua
isArray(): NO
isStruct(): YES
arguments.getClass().getName: coldfusion.runtime.ArgumentCollection
arrayLen(): 4
structCount(): 4


So, which is it, ColdFusion?  Is it not an array, or is it an array with length of 4? Railo is not so flummoxed, btw.  It sees the thing as both an array and a struct.  OpenBD sticks with CF on this one.  I think Railo has it right here.  One might argue that isArray() is telling the truth because the arguments scope is not exactly a coldfusion.runtime.Array.  But then again neither is a query column (coldfusion.sql.QueryColumn), nor ar xmlChildren (coldfusion.xml.XmlNodeArray, and indeed nor is a java.util.Vector (although granted a CF array does extend Vector), nor is a java.util.ArrayList.  Yet all of those return true from isArray().  So I reckon that's a bug (and have raised it accordingly: 3321638).

Looping

The last topic I want to cover is looping over arrays, because there's a coupla constructs in place specifically for arrays.

<cfloop>
<cfloop> provides array-specific syntax. Unfortunately Adobe messed it up a bit, because the syntax is as follows:

<cfloop  
    index = "index name" 
    array = "array" 
    ... 
</cfloop>
Well: no.  What's put in that variable is not the index, it's the element at that index.  In the context of arrays, index != element.  Even if they're both kinda related to each other, you can't just pick and choose what you call them.  It doesn't work like that.  Well it does in CF, but it oughtn't.  Oh well.

Here's an example of this in action:

<cfset numbers = ["tahi", "rua", "toru", "wha"]>
<cfloop index="number" array="#numbers#">
    <cfoutput>#number#<
</cfoutput>
</cfloop>


Output:
tahi
rua
toru
wha

So note the "index" variable isn't the index - eg: 1,2,3,4 - it's the element (value) at that index.  This wouldn't be such a problem if Adobe hadn't messed this up in a second way.  This loop doesn't actually expose the index (the actual index) to the code at all.  There's no way of knowing in that loop which index you're at, other than maintaining your own variable (which is a bit jerry-built).  Adobe could have fixed this if they had called the element variable "element", by just adding in "index" as well, eg:

<cfset numbers = ["tahi", "rua", "toru", "wha"]>
<cfloop index="index" element="number" array="#numbers#">
    <cfoutput>#index#: #number#<
</cfoutput>
</cfloop>


1: tahi
2: rua
3: toru
4: wha

But they can't fix it now, because they're already using index to hold the element.  I suppose - depending on how CFML is parsed under the hood - if the parser encounters a <cfloop> tag which has index / element / array then it could be processed differently to just index / array?  That should work.  Then they could deprecate the current implementation (I've suggested this: 3321646).

One thing to note with using <cfloop> is that once the array as "passed into it", the array being looped-over cannot be changed.  EG:

<cfloop index="number" array="#numbers#">
    <cfoutput>#number#<br /></cfoutput>
    <cfif number EQ "tahi">
        <cfset arrayAppend(numbers, "rima")>
    </cfif>
</cfloop>

Here we're trying to add a new element to the array as we go (the example seems quite contrived, but I have needed to do this before), but it doesn't do what one might expect:

tahi
rua
toru
wha
array
1tahi
2rua
3toru
4wha
5rima

The array element does actually get appended to the original array, but the one we're looping over isn't the original one.  When passing the array into <cfloop>, a copy is made of it, and the code loops over that. One cannot get around this with a normal indexed loop either, for much the same reason:

<cfset numbers = ["tahi", "rua", "toru", "wha"]>

<cfloop index="i" from="1" to="#arrayLen(numbers)#">
    <cfset number = numbers[i]>
    <cfoutput>#number#<br /></cfoutput>

    <cfif number EQ "tahi">
        <cfset arrayAppend(numbers, "rima")>
    </cfif>
</cfloop>

<cfdump var="#numbers#">

The TO value is calculated once before the loop starts.  So it's 4 when the loop starts, and it doesn't matter what happens to the array, 4 it stays (this is more obvious than the previous example, granted).

One can get around this <cfloop> limitation with an equivalent for() loop:

numbers = ["tahi", "rua", "toru", "wha"];

for (i=1; i <= arrayLen(numbers); i++){
    number = numbers[i];
    writeOutput(number & "<br />");
    if (number == "tahi"){
        arrayAppend(numbers, "rima");
    }
}
writeDump(numbers);

tahi
rua
toru
wha
rima
array
1tahi
2rua
3toru
4wha
5rima

This is because that sort of loop reevaluates the condition every iteration.  So one might think the CFscript equivalant of <cfloop> over an array might work likewise.  Here's an baseline example:

numbers = ["tahi", "rua", "toru", "wha"];

for (number in numbers){
    writeOutput(number & "<br />");
}
writeDump(numbers);


(you get the idea with the output, so I'll spare you).

However I tried this:

numbers = ["tahi", "rua", "toru", "wha"];

for (number in numbers){
    writeOutput(number & "<br />");
    if (number == "tahi"){
        arrayAppend(numbers, "rima");
    }
}
writeDump(numbers);

And that resulted in this:

tahi
The web site you are accessing has experienced an unexpected error.
Please contact the website administrator. 


The following information is meant for the website developer for debugging purposes.
Error Occurred While Processing Request
The error occurred inC:/ColdFusion10/cfusion/wwwroot/shared/git/blogExamples/arrays/forInAppend.cfm: line 4
2 : numbers = ["tahi", "rua", "toru", "wha"];
3 : 
4 : for (number in numbers){
5 :  writeOutput(number & "<br />");
6 :  if (number == "tahi"){

However if I comment out the arrayAppend() line, it all starts working again.  So there's a bit of a bug there, I think (3321655). Even if ColdFusion doesn't support appending to the array when it's being looped over, that's what the error should say.  Not just [nothing], and identifying the wrong line.

Note that Railo gets it right here, and OpenBD doesn't error, but it doesn't include the new element in the loop either.  Still: at least it doesn't error.

Err...


... that's it.

At the moment I cannot think of anything else to investigate when it comes to arrays in ColdFusion.  I gotta say it's all been a bit long-winded, but I've learned a fair bit, and I got to check out Railo and OpenBD a bit more whilst researching all this.  All platforms had their idiosyncrasies (be they bugs or non-conformities or just brain farts), but that's to be expected, and I think I've mentioned this to all the relevant parties on every occasion, so we can see what happens there.

If I've forgotten to investigate / document something, do let me know.  Do you know what though?  I've had a complete gutsful of arrays for the time being, so I will follow-up anything I've missed, but perhaps not immediately.

Hopefully for every dozen people I bored rigid with all this stuff, there's 1-2 who actually got something out of it.

Righto.

--
Adam