Recently I had to convert date time data that was recorded as a UTC datetime and display it as local time. My solution is somewhat similar to Aaron Bertran's solution here, but I thought I would provide a slightly different solution anyway.
I start out by creating a calendar of dates between January 1 and the year of the @StartDate and December 31 and the year of the @EndDate plus 1.
From the calendar table I then create another table with the UTC begin and end datetimes for the DST period and the UTC begin and end datetimes for the standard time period. In each row I also include the local datetime for the begin and end periods. I end up with 2 rows for every year (except for the last year, which has only 1 row.)
Once I have the DST table created I use logic in my code where, for example, I take the UTCCreatedDateTime and convert that to Local_DateTime by finding the row where UTCCreatedDateTime >= dst.UTCPeriodStart and UTCCreatedDateTime < dst.UTCNextPeriodStart. For each row found, I use the following logic to determine the local time. SELECT DATEADD(hh, dst.OffsetUTCToLocalCentralTime, c.UTCCreatedDateTime) AS LocalDateTime.
I live in the Central time zone so the offset from UTC is 6 hours during Central Standard Time and 5 hours during Daylight Saving Time.
Here is how I created the DST table.
DECLARE @StartDate smalldatetime = '20100101' , @EndDate smalldatetime = CAST(YEAR(GETDATE() + 1) AS char(4)) + '1231' , @OffsetHoursUTC_CST int = 6 -- Difference between UTC time and Central Standard Time ; WITH cteYears AS ( SELECT YYYY = YEAR(@StartDate) UNION ALL SELECT YYYY + 1 FROM cteYears WHERE YYYY ) , cteMonths AS ( SELECT MM = 1 UNION ALL SELECT MM + 1 FROM cteMonths WHERE MM ) , cteDays AS ( SELECT DD = 1 UNION ALL SELECT DD + 1 FROM cteDays WHERE DD ) , cteCalendar AS ( SELECT YYYY = YYYY , MM = MM , DD = DD , DateKey = (YYYY * 10000) + (MM * 100) + DD , FullDate = CAST(CAST((YYYY * 10000) + (MM * 100) + DD AS char(8)) AS DATE) , [MostRecentSunday] = DATEADD(d, -((DATEPART(WEEKDAY, CAST(CAST((YYYY * 10000) + (MM * 100) + DD AS char(8)) AS DATE)) + 6 + @@DATEFIRST) % 7), CAST(CAST((YYYY * 10000) + (MM * 100) + DD AS char(8)) AS DATE)) , [WeekNumberInMonth] = ROW_NUMBER() OVER(PARTITION BY YYYY, MM, DATENAME(WEEKDAY, CAST(CAST((YYYY * 10000) + (MM * 100) + DD AS char(8)) AS DATE)) ORDER BY (YYYY * 10000) + (MM * 100) + DD) FROM cteYears CROSS JOIN cteMonths CROSS JOIN cteDays WHERE ISDATE((YYYY * 10000) + (MM * 100) + DD) = 1 ) -- The following works for Years after 2006 -- For Years 1987 - 2006, Daylight Saving Time was in effect from the first Sunday in April through the last Sunday in October -- There is a 6 hour difference between UTC time and Central Standard Time (it is 5 hours during DST.) -- Recent DST Periods = (3/14/10 - 11/7/10, 3/13/11 - 11/6/11, 3/11/12 - 11/4/12, 3/10/13 - 11/3/13, 3/9/14 - 11/2/14) , cteDSTPeriods AS ( SELECT marDates.DateKey , marDates.FullDate , marDates.[MostRecentSunday] , marDates.[WeekNumberInMonth] , CAST(CONVERT(char(8), marDates.FullDate, 112) + ' 02:00:00' AS DATETIME) AS LocalPeriodStart , CAST(CONVERT(char(8), novDates.FullDate, 112) + ' 02:00:00' AS DATETIME) AS LocalNextPeriodStart , DATEADD(hh, 6, CAST(CONVERT(char(8), marDates.FullDate, 112) + ' 02:00:00' AS DATETIME)) AS UTCPeriodStart , DATEADD(hh, 5, CAST(CONVERT(char(8), novDates.FullDate, 112) + ' 02:00:00' AS DATETIME)) AS UTCNextPeriodStart , -5 AS OffsetUTCToLocalCentralTime FROM cteCalendar marDates INNER JOIN cteCalendar novDates ON marDates.YYYY = novDates.YYYY AND DATENAME(WEEKDAY, CAST(novDates.FullDate AS DATETIME)) = 'Sunday' AND novDates.MM = 11 AND novDates.WeekNumberInMonth = 1 WHERE DATENAME(WEEKDAY, CAST(marDates.FullDate AS DATETIME)) = 'Sunday' AND marDates.MM = 3 AND marDates.WeekNumberInMonth = 2 UNION ALL SELECT novDates.DateKey , novDates.FullDate , novDates.[MostRecentSunday] , novDates.[WeekNumberInMonth] , CAST(CONVERT(char(8), novDates.FullDate, 112) + ' 02:00:00' AS DATETIME) AS LocalPeriodStart , CAST(CONVERT(char(8), marDates.FullDate, 112) + ' 02:00:00' AS DATETIME) AS LocalNextPeriodStart , DATEADD(hh, 5, CAST(CONVERT(char(8), novDates.FullDate, 112) + ' 02:00:00' AS DATETIME)) AS UTCPeriodStart , DATEADD(hh, 6, CAST(CONVERT(char(8), marDates.FullDate, 112) + ' 02:00:00' AS DATETIME)) AS UTCNextPeriodStart , -6 AS OffsetUTCToLocalCentralTime FROM cteCalendar novDates INNER JOIN cteCalendar marDates ON novDates.YYYY + 1 = marDates.YYYY AND DATENAME(WEEKDAY, CAST(marDates.FullDate AS DATETIME)) = 'Sunday' AND marDates.MM = 3 AND marDates.WeekNumberInMonth = 2 WHERE DATENAME(WEEKDAY, CAST(novDates.FullDate AS DATETIME)) = 'Sunday' AND novDates.MM = 11 AND novDates.WeekNumberInMonth = 1 ) SELECT * FROM cteDSTPeriods ORDER BY DateKey |