dateAdd doesn't add enough hours when DST ends

Viewed 90

Recently, a coworker came up with an interesting issue. A dateTime object is created with all time parts being zero. Hours and minutes are added with dateAdd(...). This happens for October 31st 2021. The day daylight saving time ends in central Europe. We observe on our servers that one hour less is added than intended, which means the following code eventually prints the timestamp {ts '2021-10-31 07:30:00'}.

<cfset dOct31 = createDateTime( 2021, 10, 31, 0, 0, 0 )>

<cfset dOct310800 = dateAdd( 'n', 30, dOct31 )>
<cfset dOct310800 = dateAdd( 'h', 8, dOct310800 )>
<cfdump var="#dOct31#">
<cfdump var="#dOct310800#">

I run this code on TryCF and do not observe this issue with any of the available servers. I thought it may be because of the locale, but setting the TryCF code to German (Standard) doesn't change a thing.

What (JAVA) settings influence this behaviour? Can I disable this behaviour? It came as an unpleasant surprise.

1 Answers

CF uses the legacy Calendar class in date math. That class uses the jvm's default TimeZone and so it's aware of DST. Since the calculation crosses the DST boundary, it incorporates the -1hr DST adjustment on that date. AFAIK, the behavior can't be disabled.

The trycf.com results are different because that server uses the UTC TimeZone, which has no DST. However, changing it to "Europe/Berlin" does reproduce the issue described.

I'm not aware of any reliable cfml workarounds. However, the newer java.time classes should do the trick (and are currently recommended over the legacy classes CF uses anyway). The newer package includes several classes for handling date and times without a TimeZone, such as LocalDateTime:

TryCF Example

// Create CF date as usual
dOct31 = createDateTime( 2021, 10, 31, 0, 0, 0 );

// Perform "Local" version of DateAdd(...)
zoneID = createObject("java", "java.time.ZoneId").of("Europe/Berlin");
instant = createObject("java", "java.time.LocalDateTime")
    .ofInstant( dOct31.toInstant(), zoneID )
    .plusMinutes( 30 )
    .plusHours( 8 )
    .atZone( zoneID )
    .toInstant()
;
 
// Convert back into CF date {ts '2021-10-31 08:30:00'} 
dOct310800 = parseDateTime( instant.toString() );

Keep in mind other CF date functions still incorporate DST. For example a dateDiff("h") of those two dates would include the extra hour added when DST "falls back" in October.

// Result is 9 hours - not 8 - due to DST
SetTimeZone( "Europe/Berlin") ;
writeDump( 
    dateDiff("h"
        , "2021-10-31 00:00:00"
        , "2021-10-31 08:00:00"
    ) 
);

Also, if the LocalDateTime isn't valid in the current time zone, it gets automatically shifted to a valid time when converted back to a CF date.

SetTimeZone( "Europe/Berlin") ;

// 2:00 AM doesn't exist in the current time zone
zoneID = createObject("java", "java.time.ZoneId").of("Europe/Berlin");
instant = createObject("java", "java.time.LocalDateTime")
    .parse( "2021-03-28T00:00:00" )
    .plusHours( 2 )
    .atZone( zoneID )
    .toInstant()
;
 
// Result is {ts '2021-03-28 03:00:00'} 
result = parseDateTime( instant.toString() );
writeDump( result );
Related