Daylight Saving Time Error

By in Windows API on .

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 thoughts on “Daylight Saving Time Error

  1. Eric

    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. Eric

    Genius!

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

  3. Travis

    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.

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax