Thursday 16 August 2012

Arrays in CFML (part 2)

G'day:
In this article, which is part two in an ongoing series, I'll discuss the various functions CFML ships with for working with arrays. The first part described what an array is, and how to create them.

The online docs cover each of these functions too, but the coverage is pretty superficial. They cover all the syntax options and give a code example, but they don't go beyond that. Having been around the block a few times (and thus... err... ending up back where I started.. hmmmmm...), I've picked up the odd but of insight, and have explored some of the idiosyncrasies of CFML that isn't covered in the docs. I shall try to discuss why one might want to use a given function, rather than just cataloguing that it exists (which is all the official docs seem to set out to do, at times).

The docs are good for something though: they do act as a good lookup of what's available, so go have a look at then after yer done here.

Anyway, enough waffle...



Stop Press

I've just noticed that despite claiming I've covered all the array functions in ColdFusion 10 (and/or Railo), my list of entries still to write up was truncated, and I missed quite a few! So I'll be writing a follow-up article covering these ones:
I will cross reference this article with a link when it's done.

Sorry about that.

Back to the main article...

Note (and thanks to Russ for reminding me to do this) that for the purposes of running this test code, I was using the following CFML engines:

Platform Version Source
ColdFusion 10,0,0,282462 server.coldfusion.productVersion
Railo 3.3.4.003 server.railo.version
OpenBD 2.0.2 server.bluedragon.version

If I vary from these versions, I will note it inline.

arrayNew()

I think we covered enough about this last time. I include it here for completeness only. Here's some sample code:

a1 = arrayNew(1);
writeDump(var=a1, label="One-dimensional array");

a2 = arrayNew(3);
writeDump(var=a1, label="Three-dimensional array?");

writeOutput(a2.toString() & "<br >");

writeOutput(a1.equals(a2) & "<br >");
try {
    a3 = arraynew(4);  // nu-uh
    
}
catch (any e) {
    writeDump(e);
}

And the output:

One-dimensional array - array [empty]
Three-dimensional array? - array [empty]
[]
YES
struct
DETAIL[empty string]
MESSAGEArray dimension 4 must be between 1 and 3.
TYPEExpression

This does beg the question... if there's no difference between an array initialised as one dimension, and one initialised as three dimensions... what's the point of specifying it? Anyone know? For code self-documentation (which is a bit specious, given ColdFusion doesn't actually have the concept of multi-dimensional arrays)? Dunno.

I reckon they should deprecate that argument. I was gonna raise a bug for this, but I see there already is one: 3041761.

arrayAppend()

In my previous article, I demonstrated putting data at the end of an array by simply incrementing the index each time I added an element. This gets old very quickly, and isn't very maintainable if one needs to adjust the code to include an intermediary element, eg:

numbers = arrayNew(1);
numbers[1] = "one";
numbers[2] = "three"; // oops
numbers[3] = "four";
// damn... we need to go back and recode all elements from [2] onwards



Fortunately there's a computer in front of you, and you can get it to work out where the last element should go. A better way of doing the code above would be:

numbers = arrayNew(1);
arrayAppend(numbers, "one");
arrayAppend(numbers, "three");    // still "oops"
arrayAppend(numbers, "four");

// but all we need to do now is to insert one line between "one" and "three": the rest of the code is still fine

As you can see, one doesn't need to worry about what the next index is, as ColdFusion understands the idea of where the last element of the array is, and where the next one should go. Thinking about it now, perhaps a rule of thumb could be that one should only busy one's self with the index values when fetching the data, not loading it in. Obviously there'll be valid exceptions to this, but it's a reasonable starting point.

A quick digression:
Notice how the code for arrayAppend() is not of the form:

myArray = arrayAppend(myArray, newElement);

There is no assignment taking place. This is because the operation is performed directly on the argument itself, which - under the hood - is being passed to arrayAppend() by reference value, not by its actual value. I started to blather on about pass-by-value vs pass-by-reference-value in this article, but figured it warranted more discussion than would fit here, so separated it or into a different article.

Digression 2:
I see a lot of coding vagaries around this notion, eg:

temp = arrayAppend(myArray, newElement);

Don't do that. Just don't. arrayAppend() does not return a useful value, so don't store it. What's the point? It's just clutter.

Also, because there's no "left hand side" to these expressions, I often see this sort of thing:

<!--- tag-based code here --->
<cfscript>
    arrayAppend(myArray, newElement);
</cfscript>
<!--- back to tag-based code here --->

Don't do that either. Don't write three lines of code when one will do! It's completely legit to just do this:

<cfset arrayAppend(myArray, newElement)>

Sure it all compiles down to the same thing but it's just messy to be jumping in and out of CFScript at the best of times, and doing it for a single line of code is just awful.

Last digression: it's actually a bit odd that ColdFusion had implemented these functions in a pass-by-reference way, because - unlike all other complex data types in CFML - arrays are assigned by value, not by reference. See that separate discussion for details on this.

Right, back on topic: arrayAppend(). The docs page has a bit of a glitch in it in that the "parameters" table is broken: it's got an extra row in it:

Parameter Description
array Name of an array
arrayAppend A boolean value that decides if array elements are added as one element at the end of the source array. By default, it would be false thereby retaining the old behavior.
value Value to add at end of array
merge If set to true appends array elements individually to the source array. If false (default) the complete array is added as one element at the end, in the source array.

The highlighted line does not belong there (I've flagged this on the docs page too).

Also note the MERGE argument is new to CF10.

Here's some code showing both behaviours:

a1 = ["one", "two", "three", "four"];
a2 = ["tahi", "rua", "toru", "wha"];

arrayAppend(a1, a2);
writeDump(var=a1, label="The contents of a2 become a single element of a1");

a1 = ["one", "two", "three", "four"];
a2 = ["tahi", "rua", "toru", "wha"];

arrayAppend(a1, a2, true);
writeDump(var=a1, label="The contents of a2 are individually added to a1");

Which results in:

The contents of a2 become a single element of a1 - array
1one
2two
3three
4four
5
The contents of a2 become a single element of a1 - array
1tahi
2rua
3toru
4wha
The contents of a2 are individually added to a1 - array
1one
2two
3three
4four
5tahi
6rua
7toru
8wha

I think this is a good new feature in CF10.

One caveat with it is if the second argument isn't an array, then the MERGE argument is ignored. EG:

a1 = ["one", "two", "three", "four"];
st = {one="tahi", two="rua", three="toru", four="wha"};

arrayAppend(a1, st, true);
writeDump(var=a1, label="The contents of st still become a single element of a1");

This yields:

The contents of st still become a single element of a1 - array
1one
2two
3three
4four
5
The contents of st still become a single element of a1 - struct
FOURwha
ONEtahi
THREEtoru
TWOrua

Personally I'm not sure about code being "ignored"; a better behaviour here would be for it to error. Garbage in, garbage out. I dislike Adobe / ColdFusion's habit of second-guessing what I mean. It should just do what it's told (even if doing what it's told means it will error).

This new argument is also supported by Railo, but not by Open BlueDragon.

A trivial / nitpicky note. The docs say this: "Returns True, on successful completion" . Actually it returns "Yes". In CFML a string value "Yes" is a valid boolean equivalent of "true", but they should get it right.

I fished around on Google for anything else interesting about arrayAppend(), and found this comment about arrayAppend() with XML, written up by Ben Nadel. He explains it perfectly, so there's no point in repeating it here. It's worth you having a read though.

Note that OpenBD does not support the merge argument.

arrayPrepend()

This works the same as arrayAppend(), except it sticks the new element at index 1, and shimmies all the rest of them down by one. EG:

a = ["rua", "toru", "wha"];
writeDump(a);

arrayPrepend(a, "TAHI");
writeDump(a);

array
1rua
2toru
3wha
array
1TAHI
2rua
3toru
4wha


Why would one want to prepend stuff onto the beginning of an array? One use case is if one wishes to implement a stack: a data structure in which multiple items are managed in sequence, the sequence being last-in-first-out. With a stack one wants to keep track of the order that items were accumulated, but its only ever the most recent item that's processed. One could append them to the end of the array, but then one needs to keep track of which index of the last one (as it keeps changing). If one looks at the other end if the array - the beginning - one always knows what the element's index is: one. So one can just prepend elements as needs must, then when it comes to processing them, one just grabs (and removes) the first element. This technique would be irrelevant if CFML supported push and pop actions, but it doesn't, natively.

Here's an example of a LIFO stack in action. It's a simle function which converts a decimal number into a binary number. I lifted the algorithm from Wikipedia:

function decimalToBinary(numeric decimalNumber){
    var stack    = [];
    var result    = "";

    // work out all the bits
    while (decimalNumber > 0){
        var bit = decimalNumber mod 2;
        arrayPrepend(stack, bit);    // just pushing them into the front of the array is fine: we don't need to know their index, we just need them in a specific order
        decimalNumber = decimalNumber \ 2;
    }

    // pop them all back out from the front of the array, creating the binary number
    while (arrayLen(stack)){
        result &= stack[1];
        arrayDeleteAt(stack, 1);
    }
    return result;
}

i = 42;
writeOutput("Decimal: #i#; binary: #decimalToBinary(i)#");

This outputs:

Decimal: 42; binary: 101010

That said, in this just using a loop works fine too, so it's not really a compelling example of using a stack:

function decimalToBinary(numeric decimalNumber){
    var stack    = [];
    var result    = "";

    // work out all the bits
    while (decimalNumber > 0){
        var bit = decimalNumber mod 2;
        arrayAppend(stack, bit);
        decimalNumber = decimalNumber \ 2;
    }

    // pop them all back out from the front of the array, creating the binary number
    for (var i=arrayLen(stack); i > 0; i--){
        result &= stack[i];
    }
    return result;
}

i = 42;
writeOutput("Decimal: #i#; binary: #decimalToBinary(i)#");

A better example would be a case where the stack population is handled by one process, and processing the elements on the stack is handled by another process:

jobLog = "jobs";

stack = [];

thread name="queueJobs" action="run" {
    for (i=1; i <= 10; i++){
        // we need to single-thread this...
        lock name="stack" type="exclusive" timeout=1 throwontimeout=true{
            jobName = "Job ###i#";
            arrayPrepend(stack, jobName);
        }
        writeLog(file=jobLog, text="#jobName# added");
        sleep(randRange(50,150));
    }
}

thread name="processJobs" action="run" {
    while (!arrayIsEmpty(stack)) {
        sleep(randRange(100,200));
        // ... because these two operations need to be atomic
        lock name="stack" type="exclusive" timeout=1 throwontimeout=true{
            job = stack[1];
            arrayDeleteAt(stack, 1);
        }
        writeLog(file=jobLog, text="#job# removed");
    }        
}

thread action="join" name="queueJobs,processJobs" {}    // wait here until the threads are done

writeDump(stack);    // demonstrate it's empty

This is a bit more complicated. It uses separate threads to "load" and "process" jobs from a stack. I've added a random pause between each iteration of each process so there's a chance for one of them to outpace the other one, to demonstrate the jobs are processed "most recent first" irrespective how quickly they're added or processed.

Here's the output from the log:
Job #1 added
Job #2 added
Job #2 removed
Job #3 added
Job #3 removed
Job #4 added
Job #4 removed
Job #5 added
Job #5 removed
Job #6 added
Job #6 removed
Job #7 added
Job #8 added
Job #8 removed
Job #9 added
Job #10 added
Job #10 removed
Job #9 removed
Job #7 removed
Job #1 removed

Note how the threads are intermingling, but the "remove" thread always removes the most-recently-added job.

arrayInsertAt()

Sometimes one needs to stick a new element into the middle of the array, not the beginning or end. arrayInsertAt() is how we do this:

a = ["tahi", "toru"];
writeDump(a);

arrayInsertAt(a, 2, "rua");
writeDump(a);

And output:
array
1tahi
2toru
array
1tahi
2rua
3toru

There's not much to say being that. The function simply does what it says on the tin.

arraySet()

This function sets elements within a specified range of indices to a given value. I suppose this is useful if one wishes to initialise a new array to have specific values, or wipe out a range of elements. I've never needed to do either of these, so can't offer any insight a to why one might want to do it, I'm afraid. I googled about the place and found some sites they simply repeat the docs: explaining "how", but not "why". Anyway, here's how:

a = ["tahi"];
a[10] = "tekau";

arraySet(a, 3,8, "FILLER");

writeDump(a);

And the result:
array
1tahi
2[undefined array element] Element 2 is undefined in a Java object of type class coldfusion.runtime.Array.
3FILLER
4FILLER
5FILLER
6FILLER
7FILLER
8FILLER
9[undefined array element] Element 9 is undefined in a Java object of type class coldfusion.runtime.Array.
10tekau

Nothing out of the ordinary there.

One thing I did find which is worth mentioning is something Ray Camden blogged about: the expression used as the value to set the array elements to is only evaluated once. One might think one could do this to initialise an array with UUIDs:

a = [];
arraySet(a, 1,5, createUuid());
writeDump(a);


What this will do is populate the array with the same one UUID because createUuid() is only called once:

array
1EC864861-F8F8-8798-CF57ECED030C621A
2EC864861-F8F8-8798-CF57ECED030C621A
3EC864861-F8F8-8798-CF57ECED030C621A
4EC864861-F8F8-8798-CF57ECED030C621A
5EC864861-F8F8-8798-CF57ECED030C621A

As Ray points out, this leads to possibly unexpected behaviour if one populates the array with an empty struct: the array will be populated with the same struct reference, so any key/values added to one element will actually be added to the struct in the other elements too, because they're all the same struct. So best to remember that about this function, if nothing else. Example:

a = [];
arraySet(a, 1,5, {});
writeDump(a);

a[1].key = "value";

writeDump(a);

Output:
array
1
struct [empty]
2
struct [empty]
3
struct [empty]
4
struct [empty]
5
struct [empty]
array
1
struct
KEYvalue
2
struct
KEYvalue
3
struct
KEYvalue
4
struct
KEYvalue
5
struct
KEYvalue

Strangely, Railo does not behave like this with {} as the set value: only the first element has the key/value added. I'm not sure what to make of that.

arrayResize()

This is another function I've never used. Guidance in the Adobe docs says to do this immediately after creating an array that is likely to end up getting quite big. I'm on the train at the moment so can't test this, but I suspect this is because CF will adjust the underlying Vector's capacityIncrement. This will optimise the array's performance when it's being filled. I'll come back to this...

[I'm back]

Here's some sample code to demonstrate this:

a = [];
writeOutput("Elements: #arrayLen(a)#; capcity: #a.capacity()#<br />");

arrayResize(a, 1000);
writeOutput("Elements: #arrayLen(a)#; capcity: #a.capacity()#<br />");


And this outputs:
Elements: 0; capcity: 10
Elements: 1000; capcity: 1280

Note that capacity() is a method of the CF arrays underlying implementation, which extends java.util.Vector.

I did some googling about, and it seems there is a performance hit worth considering here. I'll leave it to someone else to test this out.

arrayDeleteAt()

As well as adding elements into an array, one will also need to delete elements. There's not much too say about this, other than to forewarn you of a pitfall I've fallen into, and one that I've seen a number of people also fall foul of. It's nothing significant, just a common brain-fart people tend to have. Here's some sample code:

a = ["tahi", "rua", "toru", "wha", "rima", "ono", "whitu", "waru"];
writeDump(a);

// now get rid of the first five elements
for (i=1; i <=5; i++){
    arrayDeleteAt(a, i);
}
writeDump(a);

We want to get rid of the elements at position 1-5, so we loop through them and get-rid. What's wrong with this? Well when it runs, it yields this:

array
1tahi
2rua
3toru
4wha
5rima
6ono
7whitu
8waru
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

Cannot access array element at position 5.

The array has 4 indexes. Valid positions are from 1 to 4.

Huh?

Well if we desk-check this logic, we quickly get reminded that as we delete elements, the remaining elements after the one deleted are shifted back by one so as to not leave a hole in the array, and the array gets one element smaller. So by the time we come to be deleting what was the fifth element, we don't actually have five elements left in the array. If I stick a dump just after the arrayDeleteAt() call, we would see this:

array
1tahi
2rua
3toru
4wha
5rima
6ono
7whitu
8waru
array
1rua
2toru
3wha
4rima
5ono
6whitu
7waru
array
1rua
2wha
3rima
4ono
5whitu
6waru
array
1rua
2wha
3ono
4whitu
5waru
array
1rua
2wha
3ono
4waru


(I've squished 'em together to save space)

When looking at the intermediary results like that, it's easy to see why the error was thrown.

When deleting a series of elements, either start at the highest index and move backwards through the array, or just sit at the same index and delete the same one the number of times that you have elements to delete. IE: if you want to delete five elements from the first position, you delete the first one, and what was the second one now becomes the first one, so you just delete the first element again, and now what started off being the third element is now the first element… and so on.

a = ["tahi", "rua", "toru", "wha", "rima", "ono", "whitu", "waru"];
writeDump(a);

// now get rid of the first five elements
for (i=1; i <=5; i++){
    arrayDeleteAt(a, 1);
}
writeDump(a);

array
1tahi
2rua
3toru
4wha
5rima
6ono
7whitu
8waru
array
1ono
2whitu
3waru

That all seems obvious once one thinks about it, but I bet a lot of us have been caught our by this at least once.

arrayClear()

I gotta say I didn't even know this function existed, so I had to look it up (suddenly I don't seem so insightful... oops...). [pause in typing whilst I RTFM]... OK, this seems to be one of the least useful functions in the CFML language. It does exactly what it sounds like it does: clears the array. How this differs from just reinitialising it, I dunno. If I was bring charitable, I perhaps guess it could be used to "self document" code if one explicitly wanted to show that the code was clearing an existing array (tidying up) as opposed to creating a new array (ready to use it for something else). That's clutching at straws though. Had anyone used this, and have a good use case?

Anyway, here's some example code:

a = ["tahi", "rua", "toru", "wha"];
writeDump(a);

arrayClear(a);
writeDump(a);

array
1tahi
2rua
3toru
4wha
array [empty]


arraySort()

To state the obvious, this sorts the array. As with other array functions, it acts on the array itself, rather than returning a sorted copy of the array. Reading through the docs, Adobe made a curious change to arraySort() in CF10: they added support for locale-sensitive sorting (good), but made it optional (odd), and defaulted the optionality to false (daft). It was a bug that it wasn't locale-aware to start with, but why on earth make the fixed behaviour optional? When would it ever be correct to not do a locale-aware sort? I suspect this is an example of Adobe going too far with the preservation of "backwards compatibility". Guys: if the previous behaviour was simply wrong, you don't need to preserve it when you fix an issue. That's just dumb, and wastes your time. You need to improve your purchase on the real world when it comes to this backwards compat thing.

Anyway... back to the function. It behaves like one works expect: one can specify to sort alphabetically or numerically, and ascending or descending.

Here's an example of the locale-specific stuff in action:

a = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","é","à","è","ù","â","ê","î","ô","û","ë","ï","ü","ÿ","ç"];
arraySort(a, "text", "asc", false);
writeDump(a);

arraySort(a, "text", "asc", true);
writeDump(a);

array
1a
2b
3c
4d
5e
6f
7g
8h
9i
10j
11k
12l
13m
14n
15o
16p
17q
18r
19s
20t
21u
22v
23w
24x
25y
26z
27à
28â
29ç
30è
31é
32ê
33ë
34î
35ï
36ô
37ù
38û
39ü
40ÿ
array
1a
2à
3â
4b
5c
6ç
7d
8e
9é
10è
11ê
12ë
13f
14g
15h
16i
17î
18ï
19j
20k
21l
22m
23n
24o
25ô
26p
27q
28r
29s
30t
31u
32ù
33û
34ü
35v
36w
37x
38y
39ÿ
40z

The first one seems to just be sorted by character code, the latter is sorted properly.

The docs are a bit screwed up for arraySort(). They don't mention the localeSpecific flag in the syntax description, and it's in the wrong place in the parameter table. And it's not demonstrated in the sample code. Not one of the best pieces of documentation, this one.

It's noteworthy that Railo doesn't support this setting. I've asked them about this.

It also neglects to mention one of the key additions to CF10: the ability to pass a callback to arraySort() to implement customised sorting. This is a fairly significant omission from the docs!

Here's an example of using a callback to perform more complex sorting:

a = [
    {firstName="Witi", lastName="Ihimaera"},
    {firstName="Patricia", lastName="Grace"},
    {firstName="Alan", lastName="Duff"},
    {firstName="Lee", lastName="Tamahori"},    // OK: not an author
    {firstName="Keri", lastName="Hulme"}
];

arraySort(
    a,
    function (e1, e2){
        return compare(e1.lastName, e2.lastName) > 0;
    }
);
writeDump(a);

Update:
The above code is wrong. It was correct at the time it was written, but Adobe has since fixed a bug which prevents this code from working. The return statement from the callback should read:

return compare(e1.lastName, e2.lastName);

See the subsequent article I wrote on this specific situation.


array
1
struct
FIRSTNAMEAlan
LASTNAMEDuff
2
struct
FIRSTNAMEPatricia
LASTNAMEGrace
3
struct
FIRSTNAMEKeri
LASTNAMEHulme
4
struct
FIRSTNAMEWiti
LASTNAMEIhimaera
5
struct
FIRSTNAMELee
LASTNAMETamahori

That's pretty cool.

What's not cool is that arraySort() won't deal with sparse arrays. Here's an example:

a = [];
a[1] = "tahi";
a[3] = "toru";
a[5] = "rima";

arraySort(a, "text");
writeDump(a);

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

In the ArraySort function, the array element at position 2 is not a simple value.


Seriously... if CF is going to allow sparse arrays, it has to support them. It's not even reporting the problem properly here!

Also note that the docs say if there's a non-simple value in the array, then arraySort() throws a ArraySortSimpleValueException. No it doesn't:

a = ["tahi", {}, "toru"];
try {
    arraySort(a, "text");
    writeDump(a);
}
catch (any e){
    writeDump(e);
}

Results in:
struct
Detail[empty string]
ErrNumber0
MessageIn the ArraySort function, the array element at position 2 is not a simple value.
StackTracecoldfusion.runtime.ArraySortSimpleValueException: In the ArraySort function, the array element at position 2 is not a simple value. at coldfusion.runtime.Array.ArraySort(Array.java:631) [...]
TagContext 
TypeExpression
position2

The Java code under the hood raises a "coldfusion.runtime.ArraySortSimpleValueException", but this is not bubbled back up to the ColdFusion code. Instead it's just raised as an "Expression". Unless the exception is bubbled back to the code making the call, there's no point in documenting what happens in internal code.

arraySwap()

This is another function I didn't know about. It swaps two elements of an array. I guess this is handy if one is writing one's own sorting routines. Although this is perhaps less important now that Adobe has added callback support... handy for people not on CF10 perhaps (and, indeed, I'm one of those... I've only got CF10 for testing).

a = ["tahi", "toru", "rua", "wha"];
writeDump(a);

arraySwap(a, 2, 3);
writeDump(a);

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

Perfect. One good thing about arraySwap() is that - unlike most of CF's array functions - it deals with empty elements no problems. Cool. Railo: no, it errors:

Railo 4.0.0.013 Error (expression)
Messagecan't swap values of array
DetailElement at position [2] doesn't exist in array

I've mentioned this to them, and - at their behest - raised RAILO-2048 to deal with it.

arrayIsDefined()

For the longest time, whilst one could create a sparse array, it was not possible to natively detect whether there was an element at a given index if the array, other than trying to access it and catching the error if one ensued. That sucked.

a = arrayNew(1);
a[2] = "rua";

function isArrayElementDefined(a, i){
    try {
        var temp = a[i];
        return true;
    }
    catch (any e){
        writeDump(e);
        return false;
    }    
}

writeOutput("1: #isArrayElementDefined(a, 1)#<br />");
writeOutput("2: #isArrayElementDefined(a, 2)#<br />");

Output:
1: false
2: true

The sample code here also demonstrates that CFML functions are written by Yoda. "arrayIsDefined()"? Seriously? Even with the curious word order, it's actually called the wrong thing. We're not testing if the array is defined, we're testing if an element within the array is defined. Those are two different things.

Anyway, with ColdFusion 8 came this function. And back in CF8, the guidance in the docs is this:
The index value of an element must be less than the length of the array.
And this was certainly the case, and it was "less than ideal". The function is supposed to take an array and a number and tell you if there's an element at that index. Simple as that. There's no need for the additional caveat that the index you're already unsure as to whether it exists is within some length boundary you're more than likely also not that sure about given the function call you're trying to make.

The docs still say this in CF9, but the caveat is gone in CF10. And I just tested on CF9 + CF10 both, and Adobe seemed to have fixed this glitch. Cheers Adode!

Here's some sample code to demonstrate it working fine:

a = arrayNew(1);
a[2] = "rua";

for (i=1; i <= 3; i++){
    try {
        writeOutput("#i#: #arrayIsDefined(a, i)#<br />");
    } catch (any e){
        writeDump(e);
    }
}
writeDump(a);

1: NO
2: YES
3: NO
array
1[undefined array element] Element 1 is undefined in a Java object of type class coldfusion.runtime.Array.
2rua

Cool!

arrayIsEmpty()

I'm not sure why they implemented this function, as just dung a length-check works achieve the same results. Maybe it's faster for arrays that do have length: checking a yes/no condition must be more expedient than counting how many elements there are.

Sample code:

a = ["tahi", "rua", "toru", "wha"];
writeDump(a);
writeOutput("arrayIsEmpty: #arrayIsEmpty(a)#<br />");

arrayClear(a);
writeDump(a);
writeOutput("arrayIsEmpty: #arrayIsEmpty(a)#<br />");

And output:

array
1tahi
2rua
3toru
4wha
arrayIsEmpty: NO
array [empty]
arrayIsEmpty: YES

This is another "Yoda function". Even when typing in the same code, my brain made me type in English: "isArrayEmpty()". Which, of course, is not how Yoda speaks, so it doesn't work ;-)

arrayLen()

Speaking of length checks, this is how it's done. It works well. There's not much to say about it other than that.

a = ["tahi", "rua", "toru", "wha"];
writeDump(a);
writeOutput("arrayLen: #arrayLen(a)#<br />");

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

arrayMin() / arrayMax()

These two are pretty similar so I'll group them together. There's also not much to say... arrayMin() returns the lowest, arrayMax() the highest value from an array populated solely with numeric values. They'll error if there's any non-numeric values. They'll also not cope with sparse arrays. There's no other vagaries to be aware of that I am aware of, or can find online.

a = [1,2,3,0,-3,-2,-1];
writeDump(a);
writeOutput("arrayMin: #arrayMin(a)#<br />");
writeOutput("arrayMax: #arrayMax(a)#<br />");

array
11
22
33
40
5-3
6-2
7-1
arrayMin: -3
arrayMax: 3
Changing the array to be this:

a = [1,2,3,"zero",-3,-2,-1];
// or
a = arrayNew(1);
a[2] = 2;


Causes problems:

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

Non-numeric value found.

The value at position 1 was not numeric or not defined.

I can understand it erroring with a string in there, but it should really cope with an undefined element.

arraySum() / arrayAvg()

Again, these two are similar, so I'll do 'em at the same time. They return the sum and the average of the array, respectively. The same rules as for arrayMin() and arrayMax() apply here: they will fail if there is any non-numeric data in the array.

a = [1,2,3,4,5];

writeDump(a);
writeOutput("arraySum: #arraySum(a)#<br />");
writeOutput("arrayAvg: #arrayAvg(a)#<br />");

array
11
22
33
44
55
arraySum: 15
arrayAvg: 3

arrayToList() / listToArray()


I'll group these two together because they complement each other. They're pretty commonly-used functions, so I'm sure you already know about them.

a1 = ["tahi", "rua", "toru", "wha"];
l = arrayToList(a1);
a2 = listToArray(1);
writeDump(variables);

struct
A1
array
1tahi
2rua
3toru
4wha
A2
array
1tahi
2rua
3toru
4wha
Ltahi,rua,toru,wha

Note that by default, lists don't respect empty elements, so to preserve these when converting from a list, one needs to use the "includeEmptyFields" parameter. I don't want to get into lists too much in this article, so I'll leave that topic there.

Last one...

isArray()

(I half expected Yoda to have called it "arrayIs()". Thank the Lord for small mercies).
This is easy: it returns true (sigh... OK, it returns "YES") if the argument is an array, otherwise it returns "NO". Example:

a = [];
writeOutput("Is it an array: #isArray(a)#<br />");
st = {};
writeOutput("Is it an array: #isArray(st)#<br />");


Predictable result:

Is it an array: YES
Is it an array: NO

Hmmm. This is interesting. Remember before when I said that there was no point in arrayNew() having the argument to specify the dimensions of the new array, because what was returned was just the same anyhow. It seems there is some slight difference. Check this out:

a1 = arrayNew(1);
a2 = arrayNew(2);
a3 = arrayNew(3);
writeOutput("Is a1 a two-dimensional array: #isArray(a1, 2)#<br />");
writeOutput("Is a2 a two-dimensional array: #isArray(a2, 2)#<br />");
writeOutput("Is a3 a two-dimensional array: #isArray(a3, 2)#<br />");

writeOutput("Does a1 equal a2: #a1.equals(a2)#<br />");
writeOutput("Does a2 equal a3: #a2.equals(a3)#<br />");
writeOutput("Does a3 equal a1: #a3.equals(a1)#<br />");

Result:
Is a1 a two-dimensional array: NO
Is a2 a two-dimensional array: YES
Is a3 a two-dimensional array: NO
Does a1 equal a2: YES
Does a2 equal a3: YES
Does a3 equal a1: YES

So isArray() can see a difference between these guys, but the underlying Java method equals() cannot. I dunno what to make of that.


Bloody hell: that was a lot of research and typing. it was worth it though, 'cos I learned a fair bit that I didn't know already (which, during proofreading, I noticed was a bit tautological. But I like it, so it stays). Not much of it useful, but at least now I know it. Hopefully you learned something too.

I'm afraid to say there's more to come... there's a bunch of other things that one can do with arrays, and how they interact with other data types in ColdFusion. But that might be a while away yet... I'm gonna be flat-tack at work tomorrow (hmmm: make that "later today"... it's 1am now...), and heading over to Ireland on Sat/Sun to see my son. I'm taking my laptop with me, but dunno how much time I'll have for writing, and doubt I will have an internet connection anyhow. Shudder.

But let's see how I go.

Cheers for sticking with me whilst I waded through all this.

G'night.

--
Adam

PS: "Arrays in CFML (part 3)"