ArrayPop(), ArrayShift(), And ArraySliceSafe() In Lucee CFML 5.3.6.61
I can get pretty darn far when I use the built-in Array methods in Lucee CFML 5.3.6.61. However, there are a few "utility" Array functions that I find myself writing from time to time, specifically relating to the push and pop methods that I use in JavaScript. As such, I thought it would be a fun little code kata to write them down in a more codified fashion. Especially since I typically write higher-level abstractions on top of lower-level functions in Lucee CMFL 5.3.6.61.
More than anything, I wish that Arrays in ColdFusion had .shift()
and .pop()
member methods. These methods remove the element from the head or the tail of the array, respectively, performing an in-place mutation on the given collection. More abstractly, these methods remove a value from a given index and return the value. As such, we can create .shift()
and .pop()
methods by creating a lower-level .takeAt()
method:
<cfscript>
values = [ "a", "b", "c", "d" ];
while ( values.len() ) {
dump( arrayShift( values ) );
}
echo( "<br />" );
values = [ "a", "b", "c", "d" ];
while ( values.len() ) {
dump( arrayPop( values ) );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I remove the first item from the collection and return it. This MUTATES the given
* collection.
*
* @collection I am the collection being mutated.
*/
public any function arrayShift( required array collection ) {
return( arrayTakeAt( collection, 1 ) );
}
/**
* I remove the last item from the collection and return it. This MUTATES the given
* collection.
*
* @collection I am the collection being mutated.
*/
public any function arrayPop( required array collection ) {
return( arrayTakeAt( collection, collection.len() ) );
}
/**
* I remove and return the value at the given collection index. This MUTATES the given
* collection.
*
* @collection I am the collection being mutated.
* @targetIndex I am the index of the value being removed.
*/
public any function arrayTakeAt(
required array collection,
required numeric targetIndex
) {
if ( ! collection.indexExists( targetIndex ) ) {
throw(
type = "IndexOutOfBounds",
message = "Target index is not defined.",
detail = "arrayTakeAt() requires the index (#targetIndex#) to be defined."
);
}
var value = collection[ targetIndex ];
collection.deleteAt( targetIndex );
return( value );
}
</cfscript>
As you can see, arrayShift()
and arrayPop()
are really just tiny abstractions over the underlying arrayTakeAt()
function:
arrayShift()
is justarrayTakeAt( 1 )
arrayPop()
is justarrayTakeAt( n )
And, when we run this ColdFusion code, we get the following browser output:

As you can see, both the arrayShift()
and arrayPop()
functions mutate the underlying collection and return a value at the same time. This is why the while()
loop in the demo doesn't run indefinitely.
ASIDE: This only works in Lucee CFML because arrays are passed by reference. It would be challenging to build such a function in earlier versions of ColdFusion where arrays were passed by value (as the underlying array would never be mutated).
The other big wish that I have for Arrays isn't so much a new function as it is a new behavior on an existing function. When it comes to "slicing" an Array, I wish that the built-in arraySlice()
function was more lenient about what it accepted. As of this writing, the arraySlice()
function currently throws an errors if:
- The start-index is outside the bounds of the array.
- The count takes the slice outside the bounds of the array.
Ideally, Lucee would just truncate the results in order to "degrade gracefully" with inexact arguments. To accomplish this, I often create an arraySliceSafe()
function that works like the arraySlice()
function; but, that will protect against outlier arguments within the implementation details. And, once we have a "safe" slice function, we can easily build other more abstract functions on top of it:
arrayLeft()
arrayRight()
arrayRest()
arrayCopy()
NOTE: The following demo does not incorporate any special meaning for negative indexes. I don't have a great mental model for those yet.
<cfscript>
// NOTE: The concepts of "left" and "right" on an Array mirror the existing concepts
// for Strings. Meaning, String.left( 1 ) gets index [ 1 ] on said string. As such,
// Array.left( 1 ) will also get index [ 1 ] on said array.
dump( arrayLeft( [], 3 ) );
dump( arrayLeft( [ "a" ], 3 ) );
dump( arrayLeft( [ "a", "b", "c", "d", "e", "f" ], 3 ) );
echo( "<br />" );
dump( arrayRight( [], 3 ) );
dump( arrayRight( [ "a" ], 3 ) );
dump( arrayRight( [ "a", "b", "c", "d", "e", "f" ], 3 ) );
echo( "<br />" );
dump( arrayRest( [] ) );
dump( arrayRest( [ "a", "b", "c" ] ) );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I return the N-LEADING values (at most) in the collection.
*
* @collection I am the collection being sliced.
* @count I am the number of leading values to include in the slice.
*/
public array function arrayLeft(
required array collection,
required numeric count
) {
return( arraySliceSafe( collection, 1, count ) );
}
/**
* I return the N-TRAILING values (at most) in the collection.
*
* @collection I am the collection being sliced.
* @count I am the number of trailing values to include in the slice.
*/
public array function arrayRight(
required array collection,
required numeric count
) {
var startIndex = ( collection.len() - count + 1 );
return( arraySliceSafe( collection, startIndex, count ) );
}
/**
* I return the N-1 trailing values in the collection.
*
* @collection I am the collection being sliced.
*/
public array function arrayRest( required array collection ) {
return( arraySliceSafe( collection, 2 ) );
}
/**
* I return a SHALLOW copy of the collection.
*
* @collection I am the collection being copied.
*/
public array function arrayCopy( required array collection ) {
return( arraySliceSafe( collection ) );
}
/**
* I perform a .slice() on the given collection, safely handling cases in which the
* startIndex or count go beyond the bounds of the collection - only the overlapping
* portions of the collection are returned.
*
* @collection I am the collection being sliced.
* @startIndex I am the (1-based) index at which to start slicing.
* @count I am the number of values to include in the slice.
*/
public array function arraySliceSafe(
required array collection,
numeric startIndex = 1,
numeric count = collection.len()
) {
var collectionLength = collection.len();
// NOTE: The endIndex in this case is INCLUSIVE - meaning, it is the last of the
// indices to be included in the resultant slice of values.
var endIndex = ( startIndex + count - 1 );
// If the given slice-range doesn't overlap with any of the indices in the given
// collection, then just return an empty array since no values will be sliced.
if (
! collectionLength ||
( count < 1 ) ||
( endIndex < 1 ) ||
( startIndex > collectionLength )
) {
return( [] );
}
// Clamp the slice-range down to the overlapping portion of the collection.
var safeStartIndex = max( 1, startIndex );
var safeEndIndex = min( collectionLength, endIndex );
var safeCount = ( safeEndIndex - safeStartIndex + 1 );
return( collection.slice( safeStartIndex, safeCount ) );
}
</cfscript>
I won't bother outputting the results since it's just a collection of array snippets. But, hopefully you can see that when we have a lenient slice function, some of our other Array functions become super light-weight abstractions over that slicing.
Anyway, I'm on vacation this week and I just wanted to keep the old machinery firing on the wonders of programming in Lucee CFML.
Reader Comments
wow explained very well , am going to copy it to my collection :P hope u not mind
@Tyson,
Awesome!! That would be grand :D
Post A Comment