Problems with Day() and CreateTimeSpan()

I'm attempting to use CreateTimeSpan() to hold information about how long my system will be down for maintenance given a starting date and time, and I've been running into a few interesting issues. I'd have thought that this would be a simple matter, as adding a timespan to an existing time stamp would be a useful mechanic, but it seems as though there are some hidden gotchas and, perhaps, some outright bugs.

The Problem

I wanted to store two values in my application that describe when my application will be experiencing a period of down time: the starting datetime value, and the duration of the down time as a timespan. If figure that these values will be the most useful, as I anticipate that the durations of down times will remain consistent, while only the starting date and time will be changed. In order to figure out if the current time is within the window of down time though, I'm going to have to add the timespan to the datetime to arrive at a new datetime that is the date and time at which the down time window ends. I'd think that this would be a simple process, but apparently there isn't a function in CF to accomplish this task.

Undaunted, I decided to just create my own and move on, so I started looking at what was necessary to accomplish this task. First, I'd need to be able to extract a number of values from the timespan so that I can add them each individually to my starting datetime. Given that the four values sent to CreateTimeSpan() are day, hour, minute, and second, I went immediately to Day(), Hour(), Minute(), and Second() where I discovered something interesting:

<cfset x = CreateTimeSpan(0, 0, 0, 0) />
CreateTimeSpan(0, 0, 0, 0)<br />

Second: <cfdump var="#Second(x)#"><br />
Minute: <cfdump var="#Minute(x)#"><br />
Hour: <cfdump var="#Hour(x)#"><br />
Day: <cfdump var="#Day(x)#"><br />


CreateTimeSpan(0, 0, 0, 0)
Second: 0
Minute: 0
Hour: 0
Day: 30

Really? 30? Weird... so I plugged in some other values for the "day" parameter of CreateTimeSpan() and kept getting weird values... 1 gives 31, 2 yields 1, and 10 gives 9... whats going on here? The truth became more apparent once I tried passing the timespan into DateFormat(): "30-Dec-99".

Ah ha! I'm onto something here: the timespan is actually just a datetime starting on a specific date: hours, minutes, and seconds are fine because the date starts at 0 hours, 0 minutes, and 0 seconds, but days are off because the starting date already has a day value (30).

The Solution

The simple work around for this is to use DateDiff() to compare the value of the timespan to its starting datetime value like so:

<cfset x = CreateTimeSpan(4, 3, 2, 1) />
CreateTimeSpan(4, 3, 2, 1)<br />

Second:<cfdump var="#Second(x)#"><br />
Minute:<cfdump var="#Minute(x)#"><br />
Hour:<cfdump var="#Hour(x)#"><br />
Day:<cfdump var="#Day(x)#"><br />

TimeFormat: <cfdump var="#TimeFormat(x)#"><br />
DateFormat: <cfdump var="#DateFormat(x)#"><br />
DateDiff Day: <cfdump var="#DateDiff('d', '30-Dec-99', DateFormat(x))#">

Ends up with:

CreateTimeSpan(4, 3, 2, 1)
Second: 1
Minute: 2
Hour: 3
Day: 3
TimeFormat: 03:02 AM
DateFormat: 03-Jan-00
DateDiff Day: 4

From there its a simple process to append each date part to a given datetime to find the end of the duration.



Dan G. Switzer, II's Gravatar @Jon:

Dates in CF are really just numbers (where the integer portion of the number represents the days from EPOCH.) The createTimeSpan() creates an number than can be added to a date/time to create a new time.

So, all you need is:

d = createDateTime(2011, 3, 7, 12, 0, 0);
t = createTimeSpan(0, 2, 0, 0);

newTime = d + t;

If you want to display like a date/time object, you can just do:

newTime = createODBCDateTime(d + t);

But CF should be smart enough to handle the number as a date.
Jon Hartmann's Gravatar @Dan: Thanks for the info, that certainly streamlines the process of adding a timespan to a datetime. I knew that CF used integers under the hood, but I was missing how to properly add them to each other.

This also explains why Day(CreateTimeSpan(0,0,0,0)) would return 30: CreateTimeSpan(0,0,0,0) corresponds to an integer value of 0, and if you dump DateFormat(0) you get '30-DEC-99'. I still think that it is an unexpected result to get 30 from Day(CreateTimeSpan(0,0,0,0)), whether or not its the correct interpretation.
Adam Cameron's Gravatar Unless I'm missing something, you're planning a lot more work here than you need to.

If you want to add a timespan to a datetime, simply do that: add it. You don't need to add each part separately. That's the whole idea of a timespan

Despite what the docs suggest - that createTimeSpan() returns a date/time - it doesn't, it just returns a double, wherein the value is a number of days.

This simple snippet demonstrates this:
   ts = createTimeSpan(1, 12, 0, 0);   // so that's 1.5 days, yeah?

This can be further demonstrated if one outputs the underlying datatype:

   d = createDate(2011, 3, 7);
   dt = createDateTime(2011, 3, 7, 22, 55, 42);
   ts = createTimeSpan(1, 2, 3, 4);
   public string function javaType(any o){
      return arguments.o.getClass().getName();
   writeOutput("Date (#d#): #javaType(d)#<br />");
   writeOutput("Date/time (#dt#): #javaType(dt)#<br />");
   writeOutput("Timespan (#ts#): #javaType(ts)#<br />");

What you're seeing when you perform date functions on that double is just a demonstration that CF will automatically cast a numeric to a datetime if one passes a numeric to a date function. When this happens, the numeric is treated as a date that is a number of days from CF's "zero date" which is 1899-12-30.

So to do this addition, one just needs to do this:
   dt = createDateTime(2011, 12, 13, 14, 15, 16);
   ts = createTimeSpan(1, 2, 3, 4);

   writeOutput("dt: #dt#<br />");
   writeOutput("ts: #ts#<br />");
   dtAsDouble = dt + ts;
   dtAsDateTime = createOdbcDateTime(dt + ts);

   writeOutput("As a double: #dtAsDouble# => #dateFormat(dtAsDouble, 'YYYY-MM-DD')# #timeFormat(dtAsDouble, 'HH:MM:SS')#<br />");
   writeOutput("As a date/time: #dtAsDateTime#<br />");

Adam Cameron's Gravatar PS: Your email validation fails on email addresses with plus-signs in the local part.

Jon Hartmann's Gravatar @Adam,

Thanks for the follow up on that: you're absolutely right that I was trying to do too much work, although I think your solution is actually more than is needed as well (see Dan's comment). I understand the date adding mechanics a lot better now, but I do still think its weird that Day(CreateTimeSpan(0,0,0,0)) returns 30; a DATE can't have 0 days, so 30 is fine, but a TIMESPAN can have 0 days, so 30 isn't accurate in my mind.

About the email... this is a really old BlogCFC install (3+ years). I'm looking into just replacing it with a new version/skin. I'm not sure if that will resolve the issue or not though.
Comments are not allowed for this entry.
Jon Hartmann, July 2011

I'm Jon Hartmann and I'm a Javascript fanatic, UX/UI evangelist and former ColdFusion master. I blog about mysterious error messages, user interface design questions, and all things baffling and irksome about programming for the web.

Learn more about me on LinkedIn.