Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

ArrayPop(), ArrayShift(), And ArraySliceSafe() In Lucee CFML 5.3.6.61

By Ben Nadel on
Tags: ColdFusion

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 just arrayTakeAt( 1 )
  • arrayPop() is just arrayTakeAt( n )

And, when we run this ColdFusion code, we get the following browser output:

arrayShift() and arrayPop() both consuming an array in Lucee CFML.

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

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.