Monday 17 September 2012

Arrays in Railo (appendix to the earlier 4-part series)

G'day:
Just when you thought it was safe... here's some more on arrays in ColdFusionRailo. I've already blathered on about the ins and outs of arrays in ColdFusion... well more like arrays in CFML as I cover some stuff in not only ColdFusion but also Railo and a bit on OpenBD too:
But a comment from Gert against one of my blog posts was tucked away in the back of my mind... Railo does some of its own thing when it comes to arrays (and structs, lists, etc).


I'm only just looking at this stuff for the first time right now as I type this blog article, so I am not yet prepared to say "I think this is something Adobe should add to ColdFusion", but I suspect this will be my conclusion by the end of the article.

First: a bit of background to why I wrote the above paragraph.

In one of my surveys, I asked what syntax people prefer.  More procedural syntax like this:

result = someFunction(myObject, args);

or or a more object-oriented approach:

result = myObject.someMethod(args);

CFML as found itself in a bit of an awkward place.  Historically - up to and including CF5 - CFML was a completely procedural language.  However with CFMX6.1 (let's forget CFMX6.0) it started a move into object orientation, and has moved more and more in that direction as subsequent versions come out.  However this is only in the context of developer-written code:  CFML has taken an odd route in that it provides the capability to write OO code, but CFML itself has stayed procedural.  I think this has been a serious mistake, and we've been left with a very cluttered language that adds more and more "general" functions littering the place.  There are no-fewer than 50 functions prefixed "image" (eg: imageResize()), 39 for spreadsheets, and even 20-odd for lists.  This was the wrong approach.  In CFMX6, when CFML started going OO, that should have extended both to code I wrote, as well as the code Adobe wrote.  I think all of these functions should be deprecated, and reimplemented as methods upon an object.  This would be a move to make the language more coherent.

As Gert pointed out: Railo is already making headway in that direction in Railo 4.x.

So for the sake of completeness, and also to get myself up to speed with all this stuff, I'm doing this "last" (hopefully!) article on arrays in CFML.

array()

This can be used to create a new array, much like the short-cut square-bracket notation can:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");
writeDump(daysOfWeek);
</cfscript>

Array
1
stringRahina
2
stringRatu
3
stringRaapa
4
stringRapare
5
stringRamere
6
stringRahoroi
7
stringRatapu

And can be "nested" to create multi-dimensional arrays:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
numbers = array(1, array(2, "two"), array(3, array("three", "toru")));
writeDump(numbers);
</cfscript>

Array
1
number1
2
Array
1
number2
2
stringtwo
3
Array
1
number3
2
Array
1
stringthree
2
stringtoru

I've been looking at some PHP recently, and it has an array() construct that can either be used for creating indexed arrays (like CFML ones), or associative ones (like CFML structs).  The difference being simply that if one doesn't specify a key for each element it's an indexed array; if one does specify a key, then it's an associative array.  This makes sense in PHP because to PHP both data structures are "arrays".  I wondered what Railo would do here, and tried this:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array(monday="Rahina", tuesday="Ratu", wednesday="Raapa", thursday="Rapare", friday="Ramere", saturday="Rahoroi", sunday="Ratapu");
writeDump(daysOfWeek);
</cfscript>

Array
1
stringRahina
2
stringRatu
3
stringRaapa
4
stringRapare
5
stringRamere
6
stringRahoroi
7
stringRatapu

So... um... I think that's a bug.  if it doesn't support the syntax, it should error.  It should not ignore half the code because it doesn't know what to do with it.

To be honest, I question the merits of array(), as [] is just cleaner.  I suspect array() predates [].  If so: it certainly would have been handy at the time.

append() / prepend()

These work just like arrayAppend() and arrayPrepend():

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rapare");    // Thursday
daysOfWeek.prepend("Raapa");    // Wednesday
daysOfWeek.append("Ramere");    // Friday
daysOfWeek.prepend("Ratu");        // Tuesday
daysOfWeek.append("Rahoroi");    // Saturday
daysOfWeek.prepend("Rahina");    // Monday
daysOfWeek.append("Ratapu");    // Sunday
writeDump(daysOfWeek);
</cfscript>

Array
1
stringRahina
2
stringRatu
3
stringRaapa
4
stringRapare
5
stringRamere
6
stringRahoroi
7
stringRatapu

insertAt()

This is the eqivalent of arrayInsertAt():

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", /*NO THURSDAY */ "Ramere", "Rahoroi", "Ratapu");
daysOfWeek.insertAt(4, "Rapare");
writeDump(daysOfWeek);
</cfscript>

(you'll be getting the idea of the dump by now, so I'll spare you)

set()

Equivalent of arraySet():

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina");
daysOfWeek[7] = "Ratapu";

daysOfWeek.set(3, 5, "FILLER");

writeDump(daysOfWeek);    
</cfscript>

Array
1
stringRahina
2
Empty:null
3
stringFILLER
4
stringFILLER
5
stringFILLER
6
Empty:null
7
stringRatapu

resize (arrayResize())
This just sets the size of the array as per arrayResize():

<cfscript>
a = [];
writeOutput("Initial size: #a.len()#<br />");
a.resize(1000);
writeOutput("After resize: #a.len()#<br />");
</cfscript>

Output:

Initial size: 0
After resize: 1000

deleteAt() (arrayDeleteAt())

Here I've doubled-up on Thursdays, so I need to get rid of one:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Rapare", "Ramere", "Rahoroi", "Ratapu"); // Rapare is duplicated
daysOfWeek.deleteAt(4);
writeDump(daysOfWeek);
</cfscript>

(same old output as we'd expect)

clear() (arrayClear())


<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");
daysOfWeek.clear();
writeDump(daysOfWeek);
</cfscript>

This results in an empty array.

sort() (arraySort())

Here I'm also checking whether Railo has the same collation glitch with this method that it has with arraySort() (and that CF10 works around in a very dumb way):

<cfprocessingdirective pageencoding="UTF-8">
<cfscript>
frenchLetters = ["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","é","à","è","ù","â","ê","î","ô","û","ë","ï","ü","ÿ","ç"];
frenchLetters.sort("text", "asc");
writeDump(frenchLetters);
</cfscript>

I'll truncate the output somewhat:

Array
1
stringa
2
stringb
3
stringc
4
stringd
5
stringe

[...]
20
stringt
21
stringu
22
stringv
23
stringw
24
stringx
25
stringy
26
stringz
27
stringà
28
stringâ
29
stringç

[etc]

Note that all the "extended" characters appear after Z, whereas they ought to be sorted inline with their non-accented counterparts.  I raised this with the Railo bods, but never got any feedback.  I shall chase.

swap() (arraySwap())

In this example the first two elements are transposed, so I swap() them:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Ratu", "Rahina", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu"); //  Ratu and Rahina in the wrong order
daysOfWeek.swap(1, 2);
writeDump(daysOfWeek);
</cfscript>

isDefined() (arrayIsDefined())

Here I defined an array in which I only populate the second element, and then check the existence of the first three elements.  This demonstrates the isDefined() method not only checks for non-defined elements within the bounds of the array, but also checks for elements outwith the bounds of the array.

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array();
daysOfWeek[2] = "Rahina";

for (i=1; i <= 3; i++){
    try {
        writeOutput("#i#: #daysOfWeek.isDefined(i)#<br />");
    } catch (any e){
        writeDump(e);
    }
}
</cfscript>

This outputs:

1: false
2: true
3: false

isEmpty() (arrayIsEmpty())

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");
writeOutput("daysOfWeek.isEmpty: #daysOfWeek.isEmpty()#<br />");

daysOfWeek.clear();
writeOutput("daysOfWeek.isEmpty() after daysOfWeek.clear(): #daysOfWeek.isEmpty()#<br />");
</cfscript>

Output:


daysOfWeek.isEmpty: false
daysOfWeek.isEmpty() after daysOfWeek.clear(): true

len() / min() / max() / sum() / avg()

These are all the same as their function-based equivalents:

numbers = [2,4,6,0,-5,-3,-1];
writeOutput("len(): #numbers.len()#<br />");
writeOutput("min(): #numbers.min()#<br />");
writeOutput("max(): #numbers.max()#<br />");
writeOutput("sum(): #numbers.sum()#<br />");
writeOutput("avg(): #numbers.avg()#<br />");

Output:
 
len(): 7
min(): -5
max(): 6
sum(): 3
avg(): 0.428571428571

toList() (arrayToList())

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");
writeOutput(daysOfWeek.toList());
</cfscript>

Output:

Rahina,Ratu,Raapa,Rapare,Ramere,Rahoroi,Ratapu

is() (isArray())


No.  Don't be silly.

(but yes, I did try it! ;-)

contains() / containsNoCase() / find() / findNoCase()


These all operate as their array-prefixed function equivalents work in Railo (which might be slightly differently from in ColdFusion.  See a separate article on that).

<cfprocessingdirective pageencoding="utf-8">
<cfset daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu")>
<cfoutput>
    contains("Rahina"): #daysOfWeek.contains("Rahina")#<br />
    contains("RAHINA"): #daysOfWeek.contains("RAHINA")#<br />
    <hr />
    containsNoCase("Ratu"): #daysOfWeek.containsNoCase("Ratu")#<br />
    containsNoCase("RATU"): #daysOfWeek.containsNoCase("RATU")#<br />
    <hr />
    find("Raapa"): #daysOfWeek.find("Raapa")#<br />
    find("RAAPA"): #daysOfWeek.find("RAAPA")#<br />
    <hr />
    findNoCase("Rapare"): #daysOfWeek.findNoCase("Rapare")#<br />
    findNoCase("RAPARE"): #daysOfWeek.findNoCase("RAPARE")#<br />
    <hr />
</cfoutput>

Outputs:


contains("Rahina"): 1
contains("RAHINA"): 0

containsNoCase("Ratu"): 2 containsNoCase("RATU"): 2
find("Raapa"): 3 find("RAAPA"): 0
findNoCase("Rapare"): 4 findNoCase("RAPARE"): 4

findAll() / findAllNoCase()

These are a rare misfire from Railo, I think.  They work by taking a callback function as the argument, and depending on whether the callback returns true or false, adds the match to an array that is returned.  Why do I say this is a misfire?  Well here's some example code:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");

daysOfWeek.append("RAMERE"); // append another "similar" entry, but we expect not to match it due to doing a case-senstive match
matches = daysOfWeek.findAll(    // so should be case-sensitive, given the method name...
    function(element){
        return !compare(element, "Ramere");    // ...but it's up to our callback to determine whether it's case-sensitive!
    }
);
writeDump(var=matches, label='daysOfWeek.findAll("Ramere")');

daysOfWeek.append("RAHOROI");    // append another "similar" entry. We DO expect to match it due to doing a case-insenstive match
matches = daysOfWeek.findAllNoCase(
    function(element){
        return element == "RAHOROI";    // shoud be case-insensitive
    }
);

writeDump(var=matches, label='daysOfWeek.findAllNoCase("RAHOROI")');

writeDump(daysOfWeek);
</cfscript>

And the output:

daysOfWeek.findAll("Ramere")
Array
1
number5
daysOfWeek.findAllNoCase("RAHOROI")
Array
Array
1
stringRahina
2
stringRatu
3
stringRaapa
4
stringRapare
5
stringRamere
6
stringRahoroi
7
stringRatapu
8
stringRAMERE
9
stringRAHOROI

Firstly: what's the point of having both findAll() and findAllNoCase(), if it's up to the callback code to do the comparison?  Note that the findAll() (which should be case-sensitive) needs to use compare() to do a case-sensitive comparison.  Whilst this is correct, it demonstrates that if I just did an equality evaluation there, then the result from findAll() would not actually be case-sensitive. So there's no point in having two functions here: the differentiation between case-sensitivity in the function names is meaningless (and misleading).

Secondly: it doesn't seem like findAllNoCase() even works.  No matter what I do, I just get an empty array back.

Thirdly: aren't these doing exactly the same thing as filter() (see below)?


That said, as I can find no docs for these (or the function versions) I do not know if I am using them correctly or not.  I've made a reasonable educated guess though, I think?

filter() (arrayFilter())


This does what findAll() and findAllNoCase() don't quite implement sensibly:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");

matches = daysOfWeek.filter(
    function(element){
        return reFindNoCase("^Rat", element);
    }
);
writeDump(var=matches);
</cfscript>

Array
1
stringRatu
2
stringRatapu


This makes more sense than its "findAll" counterparts: it just does a filter (whatever that filter might be, in this case: days that start with "rat") on each element, and either includes or discards them from the returned array.

delete() (arrayDelete())

As mentioned in the other article about array functions, in which I cover arrayDelete(), this function deletes elements of an array by value, rather than by index.  The utility of this functionality is questionable, I think.  Anyway, here it is:


<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "DELETEME", "Ratu", "DELETEME", "Raapa", "DELETEME", "Rapare", "DELETEME", "Ramere", "DELETEME", "Rahoroi", "DELETEME", "Ratapu", "DELETEME");

daysOfWeek.delete("DELETEME", "ALL");
writeDump(daysOfWeek);
</cfscript>

Array
1
stringRahina
2
stringRatu
3
stringRaapa
4
stringRapare
5
stringRamere
6
stringRahoroi
7
stringRatapu

Note I specified "ALL".  Had I not, just the first match would have been deleted.  This is something that ColdFusion doesn't have that I lamented the absence of.  Railo does indeed support this.  Well done.

each() (arrayEach())


This is similar to filter(), except it just transforms the element value.  It does not act on the array itself, just the element.  This reduces its utility in my view.

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");

daysOfWeek.each(
    function(element){
        param name="i" default=1;
        writeOutput("#i++# #uCase(element)#<br />");
    }
);
</cfscript>

So this upper-cases the element value, outputting it and its index (which I have to calculate by hand, rather than Railo passing it to me. Grumble).

1 RAHINA
2 RATU
3 RAAPA
4 RAPARE
5 RAMERE
6 RAHOROI
7 RATAPU

slice() (arraySlice())


This chops out a chunk of an array and returns it:


<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");
writeDump(var=daysOfWeek, label="Original data");

new = daysOfWeek.slice(2, 3);
writeDump(var=new, label="2,3,4 (Ratu-Rapare)");

new = daysOfWeek.slice(4);
writeDump(var=new, label="4- (Rapare-Ratapu)");

new = daysOfWeek.slice(-5, 3); 
writeDump(var=new, label="2,3,4 (Ratu-Ramere)");
</cfscript>

Original data
Array
1
stringRahina
2
stringRatu
3
stringRaapa
4
stringRapare
5
stringRamere
6
stringRahoroi
7
stringRatapu
2,3,4 (Ratu-Rapare)
Array
1
stringRatu
2
stringRaapa
3
stringRapare
4- (Rapare-Ratapu)
Array
1
stringRapare
2
stringRamere
3
stringRahoroi
4
stringRatapu
2,3,4 (Ratu-Ramere)
Array
1
stringRatu
2
stringRaapa
3
stringRapare

Others

Railo also has a few array functions that ColdFusion doesn't have:

reverse() (arrayReverse())


This does what it says on the tin:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");

reversed = daysOfWeek.reverse();
writeDump(var=reversed);
</cfscript>

Array
1
stringRatapu
2
stringRahoroi
3
stringRamere
4
stringRapare
5
stringRaapa
6
stringRatu
7
stringRahina

I fell into a trap here.  I presumed this function works like other array functions tend to, and reverses the array inline, rather than returning a different array.  IE, this is the code I initially tried:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");

daysOfWeek.reverse();
writeDump(var=daysOfWeek);
</cfscript>

Given the way pretty much all other array functions work, I think this discrepancy might count as a bug.

merge (arrayMerge())


This seems to do what CF10 does with arrayAppend(), by adding the MERGE argument:

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
weekdays = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere");
weekend = array("Rahoroi", "Ratapu");

daysOfWeek = weekdays.merge(weekend);
writeDump(var=daysOfWeek);
</cfscript>

(This outputs the whole week, in case it needed clarification ;-)

first() / last() / mid() (arrayFirst() / arrayLast() / arrayMid())

These do what they sound like.

<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");

results.first = daysOfWeek.first();
results.last = daysOfWeek.last();
results.mid = daysOfWeek.mid(2,5);
writeDump(results);
</cfscript>

Struct
FIRST
stringRahina
LAST
stringRatapu
MID
Array
1
stringRatu
2
stringRaapa
3
stringRapare
4
stringRamere
5
stringRahoroi

last() I can understand.  It saves one having to do this: myArray[myArray.len()]. But first()?  Seriously?  Going myArray.first() is preferable to myArray[1]?  No, it's not.  So that's a waste of time.  Equally mid() is a less-functional version of slice().  Why does Railo also need mid()?  I suspect the answer is "for completeness".  I s'pose.

indexExists() (arrayIndexExists())

Seems to be the same as isDefined().  Waste of time.  Well one of them is.  This one probably has a more sensible name.  Perhaps it - or arrayIndexExists() - predates arrayIsDefined(), the latter only being implemented for ColdFusion compat?

toStruct() (arrayToStruct())


<cfprocessingdirective pageencoding="utf-8">
<cfscript>
daysOfWeek = array("Rahina", "Ratu", "Raapa", "Rapare", "Ramere", "Rahoroi", "Ratapu");

result = daysOfWeek.toStruct();
writeDump(var=result);
</cfscript>

I seriously question the merits of this too, actually.  It simply does this:

Struct
1
stringRahina
2
stringRatu
3
stringRaapa
4
stringRapare
5
stringRamere
6
stringRahoroi
7
stringRatapu

I guess there are situations wherein some other method or something is expecting a struct and you have an array, so what're you to do?  Use toStruct(), obviously.  Hmmm.  I think it's seldom going to be the case where the result of a toStruct() call is going to be useful for anything.  I presume there was something in mind when this function was written though.

I think that's it.  I've gone through all the functions I listed in parts 2&3 of the original array coverage, as well as snooped around for Railo-only functions.

Obviously I've highlighted where I think there are some glitches here, but on the whole I think it's jolly good.  Hopefully this'll get onto Adobe's radar, and it'll all find its way into CF11. I should be in a position to... err... "influence" this, and I'll try my hardest.

Good work, Railo dudes.

--
Adam