Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Bruno Lopes and Simon Free
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Bruno Lopes Simon Free ( @simonfree )

Code Kata: Parsing Time Spans In ColdFusion

By on
Tags:

In ColdFusion, the createTimeSpan() function is used to define a duration. This is often used to help define properties like the application and session idle timeouts. In ColdFusion, a time span is expressed as a number of "fractional days". So, for example, a time span of one day would be expressed as 1; and, a time span of 12 hours (half a day) would be expressed as 0.5. As a fun Sunday morning code kata, I wanted to create a user defined function (UDF) that parses a time span back into its original inputs.

When you invoke the createTimeSpan() function, you pass in four arguments:

  • Days
  • Hours
  • Minutes
  • Seconds

My goal here is to, when given a fractional number of days, parse that value back into the aforementioned arguments (days, hours, minutes, seconds). We can do this by converting the fractional days into a total number of seconds. And then, start applying logic around divisors and remainders.

Here's the user defined function that I came up with:

<cfscript>

	/**
	* I parse the given ColdFusion timespan back into its inputs.
	*/
	public struct function parseTimeSpan( required numeric timespan ) {

		var inputs = [
			days: 0,
			hours: 0,
			minutes: 0,
			seconds: 0
		];
		var secondsPerDay = 86400;
		var secondsPerHour = 3600;
		var secondsPerMinute = 60;

		// Since we're dealing with fractional days, we may end up with fractional
		// seconds. Rounding to the closest integer seems to give us the best results.
		var remainder = round( timespan * secondsPerDay );

		// Extract days.
		inputs.days = fix( remainder / secondsPerDay );
		remainder -= ( inputs.days * secondsPerDay );

		// Extract hours.
		inputs.hours = fix( remainder / secondsPerHour );
		remainder -= ( inputs.hours * secondsPerHour );

		// Extract minutes.
		inputs.minutes = fix( remainder / secondsPerMinute );
		remainder -= ( inputs.minutes * secondsPerMinute );

		// Seconds is whatever is left.
		// --
		// NOTE: Using fix() to cope with any floating point fuzziness.
		inputs.seconds = fix( remainder );

		return inputs;

	}

</cfscript>

As you can see, we convert the number of days into a total number of seconds (remainder). Then, we keep dividing by various buckets-of-seconds to extract the days, hours, and minutes. The seconds input is then whatever is remaining at the end.

Let's try to use this in both Lucee CFML and Adobe ColdFusion:

<cfscript>

	// Include our user defined function (UDF).
	include "./parse-time-span.cfm";

	timespan = createTimeSpan( 1, 2, 3, 4 );

	writeDump(
		var = timespan,
		label = "TimeSpan"
	);
	writeDump(
		var = parseTimeSpan( timespan ),
		label = "Parsed Inputs"
	);

</cfscript>

When we run this, we get slightly different outputs in the two different CFML engines:

The result of parsing a time span in both Lucee CFML and Adobe ColdFusion.

In both CFML engines, we're able to take the given time span and calculate the original inputs (1, 2, 3, and 4). However, note that Adobe ColdFusion represents the value as a decimal and Lucee CFML represents it as higher-level abstraction.

Our ColdFusion code doesn't really care about this differentiated representation. However, there are some implications. For example, if we try to test the type of the time span value, we do get different outcomes:

<cfscript>

	ts = createTimeSpan( 1, 0, 30, 0 );

	writeDump( isNumeric( ts ) );
	writeDump( getMetadata( ts ).name );

</cfscript>

In Adobe ColdFusion, this gives us:

YES
java.lang.Double

And, in Lucee CFML, this gives us:

false
lucee.runtime.type.dt.TimeSpanImpl

So, while Lucee CFML will happily convert the time span to a number during maths, it's not represented as a number natively. Nor does it pass an isNumeric() check.

It turns out, this is not the only difference between the two CFML engines. While Adobe ColdFusion allows for fractional arguments, Lucee CFML will cast all arguments to integers. To test this, we can try to create a time span with 1.5 days:

<cfscript>

	writeDump( +createTimeSpan( 1.5, 0, 0, 0 ) );

</cfscript>

Since Lucee CFML is using a different abstraction, I'm using the + operator to cast the value to a number. And, when we run this in Adobe ColdFusion, we get the following:

1.5

And, in Lucee CFML, we get:

1

As you can see, Lucee CFML truncated the 1.5 argument. If you look at the createTimeSpan() documentation, the days argument is defined as an Integer. As such, this divergence in behavior is likely a bug in the Adobe ColdFusion implementation (in that it is looser in what it accepts).

With all that said, let's try putting our parsing function through a more extensive test. In the following ColdFusion code, I'm going to use nested loops to provide a wide range of inputs:

<cfscript>

	// Include our user defined function (UDF).
	include "./parse-time-span.cfm";

	counter = 0;

	// Iterate 0...10 over days, hours, minutes, seconds.
	for ( d = 0 ; d <= 10 ; d++ ) {
		for ( h = 0 ; h <= 10 ; h++ ) {
			for ( m = 0 ; m <= 10 ; m++ ) {
				for ( s = 0 ; s <= 10 ; s++ ) {

					timespan = createTimeSpan( d, h, m, s );
					inputs = parseTimeSpan( timespan );
					counter++;

					if (
						( d != inputs.days ) ||
						( h != inputs.hours ) ||
						( m != inputs.minutes ) ||
						( s != inputs.seconds )
						) {

						writeDump( inputs );
						writeDump( d );
						writeDump( h );
						writeDump( m );
						writeDump( s );
						abort;

					}

				}
			}
		}
	}

	writeOutput( "Done! #numberFormat( counter )# tests executed successfully!" );

</cfscript>

As you can see, we have nested CFLoop tags that iterate 0...10 (inclusive) for all four createTimeSpan() inputs. And, if we find any mismatch between the original inputs and the parsed inputs, we abort the request.

When we run this CFML through both Lucee CFML and Adobe ColdFusion, we get the following output:

Done! 14,641 tests executed successfully!

Both engines work the same; and, our user defined function was able to successfully parse all time span values back into their original set of inputs.

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

Reader Comments

208 Comments

This was fun! My first thought was that createTimeSpan(0, 36, 0, 0) would parse to createTimeSpan(1, 12, 0, 0) in both, but in Lucee you could actually get the original input configuration rather than just the logical one.

15,688 Comments

@Chris,

Yeah, I didn't dig too much into the Lucee class that is created under the hood. But, when I dumped-out the getMetadata(timespan), it did seem to have methods like .getMinutes() and .getHours(). So, it does seem to expose the original arguments.

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