Skip to main content
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Timothy Farrar
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Timothy Farrar

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

By on
Tags:

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.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel