Code Kata: Parsing Time Spans In ColdFusion
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:
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
This was fun! My first thought was that
createTimeSpan(0, 36, 0, 0)
would parse tocreateTimeSpan(1, 12, 0, 0)
in both, but in Lucee you could actually get the original input configuration rather than just the logical one.@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 →