Daylight Saving Time Error

It’s DST here in the US and I couldn’t be happier. I don’t care if my drive to work is pitch black, but the drive home? That’s another story.

One thing I learned since the clocks sprung forward is that the GetTimeZoneInformation API doesn’t work the way I thought. The TIME_ZONE_INFORMATION return type has a Bias property. Bias tells you how many minutes you are away from GMT. Or so I thought. It actually tells you how many minutes you are from GMT in standard time. The TIME_ZONE return value tells you if it’s daylight saving time or standard time. So you have to take both into account to get the correct time.

Here’s the API declaration

Private Type SYSTEMTIME
    wYear As Integer
    wMonth As Integer
    wDayOfWeek As Integer
    wDay As Integer
    wHour As Integer
    wMinute As Integer
    wSecond As Integer
    wMilliseconds As Integer
End Type


Private Type TIME_ZONE_INFORMATION
    Bias As Long
    StandardName(0 To 31) As Integer
    StandardDate As SYSTEMTIME
    StandardBias As Long
    DaylightName(0 To 31) As Integer
    DaylightDate As SYSTEMTIME
    DaylightBias As Long
End Type


''''''''''''''''''''''''''''''''''''''''''''''
' These give symbolic names to the time zone
' values returned by GetTimeZoneInformation .
''''''''''''''''''''''''''''''''''''''''''''''

Private Enum TIME_ZONE
    TIME_ZONE_ID_INVALID = 0        ' Cannot determine DST
    TIME_ZONE_STANDARD = 1          ' Standard Time, not Daylight
    TIME_ZONE_DAYLIGHT = 2          ' Daylight Time, not Standard
End Enum


Private Declare Function GetTimeZoneInformation Lib "kernel32" _
    (lpTimeZoneInformation As TIME_ZONE_INFORMATION) As Long

Private Declare Sub GetSystemTime Lib "kernel32" _
    (lpSystemTime As SYSTEMTIME)

And the updated procedure:

Public Function ConvertTimeToLocal(ByVal dtTime As Date, ByVal sZone As String) As Date
   
    Dim tzi As TIME_ZONE_INFORMATION
    Dim tz As TIME_ZONE
    Dim lGmtOff As Long
   
    tz = GetTimeZoneInformation(tzi)
   
    Select Case UCase(sZone)
        Case "EDT"
            lGmtOff = -4
        Case "EST", "CDT"
            lGmtOff = -5
        Case "CST", "MDT"
            lGmtOff = -6
        Case "MST", "PDT"
            lGmtOff = -7
        Case "PST"
            lGmtOff = -8
        Case vbNullString
            lGmtOff = -tzi.Bias / 60
    End Select
   
    If tz = TIME_ZONE_DAYLIGHT Then lGmtOff = lGmtOff - 1
   
    ConvertTimeToLocal = dtTime - (TimeSerial(0, tzi.Bias, 0) + TimeSerial(lGmtOff, 0, 0))
   
End Function

I also added a UCase around the zone because it’s just stupid not to have that. Enjoy saving the daylight, but remember you’ll owe it back this fall.

Update:

That’s why we write tests people.

Public Function ConvertTimeToLocal(ByVal dtTime As Date, ByVal sZone As String) As Date
   
    Dim tzi As TIME_ZONE_INFORMATION
    Dim tz As TIME_ZONE
    Dim lGmtOff As Long
    Dim lBias As Long
   
    tz = GetTimeZoneInformation(tzi)
   
    lBias = tzi.Bias
    If tz = TIME_ZONE_DAYLIGHT Then lBias = lBias - 60
   
    Select Case UCase(sZone)
        Case "EDT"
            lGmtOff = -4
        Case "EST", "CDT"
            lGmtOff = -5
        Case "CST", "MDT"
            lGmtOff = -6
        Case "MST", "PDT"
            lGmtOff = -7
        Case "PST"
            lGmtOff = -8
        Case vbNullString
            lGmtOff = -tzi.Bias / 60
            If tz = TIME_ZONE_DAYLIGHT Then lGmtOff = lGmtOff + 1
    End Select
   
    ConvertTimeToLocal = dtTime - TimeSerial(lGmtOff, 0, 0) - TimeSerial(0, lBias, 0)
   
End Function

Public Sub Test_ConvertTimeToLocal()
   
    Dim dtTestTime As Date
   
    dtTestTime = TimeSerial(9, 46, 13)
   
    Debug.Assert ConvertTimeToLocal(dtTestTime, vbNullString) - TimeSerial(9, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "CDT") - TimeSerial(9, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "EST") - TimeSerial(9, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "EDT") - TimeSerial(8, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "CST") - TimeSerial(10, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "MDT") - TimeSerial(10, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "MST") - TimeSerial(11, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "PDT") - TimeSerial(11, 46, 13) < TimeSerial(0, 0, 1)
    Debug.Assert ConvertTimeToLocal(dtTestTime, "PST") - TimeSerial(12, 46, 13) < TimeSerial(0, 0, 1)

End Sub

6 Comments

  1. Eric says:

    Is there a way to determine if a certain date was in daylight savings or not? I have a need to look at a historical date and make that determination.

  2. Dick Kusleika says:

    It looks like the GetTimeZoneInfomationForYear Windows API will give you the information you need to calculate it.

    http://msdn.microsoft.com/en-us/library/windows/desktop/bb540851%28v=vs.85%29.aspx

  3. Eric says:

    Genius!

    Thanks so much! I learned a lot with this post and the hint about GetTimeZoneInformationForYear!

  4. Doug Jenkins says:

    How come you are saving daylight up there while we are still saving daylight down here?

    There won’t be enough to go round, will there?

  5. Travis says:

    I was wondering if Eric got the GetTimeZoneInfomationForYear API to work. I have been trying but I don’t understand the Windows API calls well enough.

Posting code or formulas in your comment? Use <code> tags!

  • <code lang="vb">Block of code goes here</code>
  • <code lang="vb" inline="true">Inline code goes here</code>
  • <code>Formula goes here</code>

Leave a Reply

Here's how to update your reports of company and nearly any web data: