Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Michael Offner-Streit and Tanja Stadelmann and Gert Franz and Pierre-Olivier Chassay and Paul Klinkenberg and Marcos Placona
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Michael Offner-Streit Tanja Stadelmann Gert Franz Pierre-Olivier Chassay Paul Klinkenberg Marcos Placona

Considering A Numeric Range / Sequence Data Structure In ColdFusion

By
Published in Comments (10)

I am not sure if I would ever need something like this in a production application, but when I'm toying around with ideas in ColdFusion, it's not uncommon for me to want to iterate over a sequence of numbers. I know that other languages have the concept of a first class "Range" or "Sequence" structure. And, it seems like something that might be of some value in ColdFusion as well. As such, I wanted to try implementing a numeric range / sequence data structure in Lucee CFML.

The concept of a Range is fairly straightforward as I understand it: there's an upper-bound and a lower-bound value; and then, there's functionality for iterating over the values in that static range. For example, you might want to call .each() or .map() on a range to operator on each value within the range.

I think the easiest way to implement this would be store the upper/lower bound values. And then, as needed, generate an internal Array which would allow us to call the native .each() and .map() member methods. This would, in turn, bake-in all the native parallel iteration capabilities on top of our numeric range.

Here's my simple implementation which allows for both ascending and descending ranges:

component
	accessors = true
	output = false
	hint = "I provide an iterable sequential numeric range."
	{

	// Define properties for GETTERS.
	property name="endValue" setter=false;
	property name="startValue" setter=false;

	/**
	* I initialize the range with the given outliers (inclusive).
	*/
	public void function init(
		required numeric startValue,
		required numeric endValue
		) {

		variables.startValue = fix( arguments.startValue );
		variables.endValue = fix( arguments.endValue );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I iterate over the values in the range using the given operator.
	*/
	public any function each(
		required function operator,
		numeric step = 1,
		boolean parallel = false,
		numeric maxThreads = 20
		) {

		// NOTE: While it may be more "expensive" to convert to an array first, it makes
		// the code significantly easier to write. And, it allows us to leverage the
		// native array methods, including the parallelization of the operator.
		toArray( step ).each( operator, parallel, maxThreads );

		return( this );

	}


	/**
	* I map the values in the range onto an array using the given operator.
	*/
	public array function map(
		required function operator,
		numeric step = 1,
		boolean parallel = false,
		numeric maxThreads = 20
		) {

		// NOTE: While it may be more "expensive" to convert to an array first, it makes
		// the code significantly easier to write. And, it allows us to leverage the
		// native array methods, including the parallelization of the operator.
		return( toArray( step ).map( operator, parallel, maxThreads ) );

	}


	/**
	* I convert the range into an array using the given step increment.
	*/
	public array function toArray( numeric step = 1 ) {

		var values = [];

		step = abs( fix( step ) );

		// Range is increasing.
		if ( startValue <= endValue ) {

			for ( var value = startValue ; value <= endValue ; value += step ) {

				values.append( value );

			}

		// Rance is decreasing.
		} else {

			for ( var value = startValue ; value >= endValue ; value -= step) {

				values.append( value );

			}

		}

		return( values );

	}

}

As you can see, there's really nothing to it. The only method of any consequence is the .toArray(step) method. This is what generates the intermediary Array object on which we get to call the native array methods. The other methods - .each() and .map() - then become little more than proxies to the underlying .toArray() method.

In testing / experimentation code, we could then use this to generate numeric ranges for whatever fun purposes we need. For example, we could easily generate the lyrics to the 99 Bottles of Beer children's song:

<cfscript>

	range = new Range( 99, 0 );

	// Generate the lyrics to the "99 Bottles of Beer" song.
	lyrics = range.map(
		( value ) => {

			if ( ! value ) {

				return( "Go home!" );

			}

			return(
				"#value# bottles of beer on the wall. " &
				"#value# bottles of beer. " &
				"Take one down, pass it around, #(value - 1 )# bottles of beer on the wall."
			);

		}
	);

	dump( "Start: " & range.getStartValue() );
	dump( "End: " & range.getEndValue() );
	dump( lyrics );

</cfscript>

As you can see, we're creating a descending numeric range from 99 to 0 (inclusive) and then using the .map() function to map the range values onto individual lyrics. And, when we run this Lucee CFML code, we get the following output:

Again, I am not sure that I would ever need something like this in a production ColdFusion app. Though, maybe I could see it being helpful when generating grid-like interfaces, like a Calendar. But, I would definitely make use of this in a lot of testing / experimentation code.

Lucee CFML CFLoop has a times Attribute

As an aside, I wanted to remind people that in Lucee CFML, the CFLoop tag has a times attribute that allows for arbitrary execution of a block of code:

<cfscript>

	loop times = 10 {

		echo( "Hello! <br />" );

	}

</cfscript>

I use this often enough in experimentation code; which is, I think, evidence that something like a Range and rangeNew() feature in ColdFusion would be helpful.

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

Reader Comments

15,848 Comments

@Chris,

fix() just chops off the decimal value just in case someone passes in anything other than an integer.

fix( 1.3 ) = 1
fix( 1.8 ) = 1

It's different from round() in that it doesn't round up ever, it just chops off the decimal.

15,848 Comments

@Matt,

Great example! I completely forgot there was even an int() function. The other function that I use all the time is val() for converting strings (like url.id) into numeric values.

238 Comments

@Matt,

Thanks for the links. Good stuf! I too use val() often to coalesce values to numeric. I had not been aware of fix() before this article. Always learning!

15,848 Comments

@Dawesi,

I've don't a "hello world" with Boxlang, but that's about it. Over in the Working Code Discord chat, I know someone is actively trying to convert their entire app over to Boxlang so that they can be running on Java 21. And, I think they've just about got it working. It's definitely something I'm going to keep my eye on.

Post A Comment — I'd Love To Hear From You!

Post a Comment

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