If you need to convert dates other than today to different time zones, you have to deal with summer savings. I need a solution that could be made without worrying about the database version, without using stored functions and something that could be easily ported to Oracle.
I think Warren is on the right track with getting the right dates for daylight, but to make it more useful for several time zones and different rules for countries and even a rule that changed in the USA from 2006 to 2007, here's an option solutions. Please note that we have not only time zones, but also central Europe. Central Europe follows the last Sunday of April and the last Sunday of October. You will also notice that the USA in 2006 follows the old first Sunday in April, the last Sunday in October.
This SQL code may look a little ugly, but just copy and paste it into SQL Server and give it a try. Please note that there are 3 sections for many years, time zones and rules. If you want another year, just add it to the year of the union. The same goes for another time zone or rule.
select yr, zone, standard, daylight, rulename, strule, edrule, yrstart, yrend, dateadd(day, (stdowref + stweekadd), stmonthref) dstlow, dateadd(day, (eddowref + edweekadd), edmonthref) dsthigh from ( select yrs.yr, z.zone, z.standard, z.daylight, z.rulename, r.strule, r.edrule, yrs.yr + '-01-01 00:00:00' yrstart, yrs.yr + '-12-31 23:59:59' yrend, yrs.yr + r.stdtpart + ' ' + r.cngtime stmonthref, yrs.yr + r.eddtpart + ' ' + r.cngtime edmonthref, case when r.strule in ('1', '2', '3') then case when datepart(dw, yrs.yr + r.stdtpart) = '1' then 0 else 8 - datepart(dw, yrs.yr + r.stdtpart) end else (datepart(dw, yrs.yr + r.stdtpart) - 1) * -1 end stdowref, case when r.edrule in ('1', '2', '3') then case when datepart(dw, yrs.yr + r.eddtpart) = '1' then 0 else 8 - datepart(dw, yrs.yr + r.eddtpart) end else (datepart(dw, yrs.yr + r.eddtpart) - 1) * -1 end eddowref, datename(dw, yrs.yr + r.stdtpart) stdow, datename(dw, yrs.yr + r.eddtpart) eddow, case when r.strule in ('1', '2', '3') then (7 * CAST(r.strule AS Integer)) - 7 else 0 end stweekadd, case when r.edrule in ('1', '2', '3') then (7 * CAST(r.edrule AS Integer)) - 7 else 0 end edweekadd from ( select '2005' yr union select '2006' yr -- old us rules UNION select '2007' yr UNION select '2008' yr UNION select '2009' yr UNION select '2010' yr UNION select '2011' yr UNION select '2012' yr UNION select '2013' yr UNION select '2014' yr UNION select '2015' yr UNION select '2016' yr UNION select '2017' yr UNION select '2018' yr UNION select '2018' yr UNION select '2020' yr UNION select '2021' yr UNION select '2022' yr UNION select '2023' yr UNION select '2024' yr UNION select '2025' yr UNION select '2026' yr ) yrs cross join ( SELECT 'ET' zone, '-05:00' standard, '-04:00' daylight, 'US' rulename UNION SELECT 'CT' zone, '-06:00' standard, '-05:00' daylight, 'US' rulename UNION SELECT 'MT' zone, '-07:00' standard, '-06:00' daylight, 'US' rulename UNION SELECT 'PT' zone, '-08:00' standard, '-07:00' daylight, 'US' rulename UNION SELECT 'CET' zone, '+01:00' standard, '+02:00' daylight, 'EU' rulename ) z join ( SELECT 'US' rulename, '2' strule, '-03-01' stdtpart, '1' edrule, '-11-01' eddtpart, 2007 firstyr, 2099 lastyr, '02:00:00' cngtime UNION SELECT 'US' rulename, '1' strule, '-04-01' stdtpart, 'L' edrule, '-10-31' eddtpart, 1900 firstyr, 2006 lastyr, '02:00:00' cngtime UNION SELECT 'EU' rulename, 'L' strule, '-03-31' stdtpart, 'L' edrule, '-10-31' eddtpart, 1900 firstyr, 2099 lastyr, '01:00:00' cngtime ) r on r.rulename = z.rulename and datepart(year, yrs.yr) between firstyr and lastyr ) dstdates
For rules, use 1, 2, 3, or L for the first, second, third, or last Sunday. Part of the date gives the month and depending on the rule, the first day of the month or the last day of the month for rule type L.
I put the above request in a view. Now, anytime I need a date with a timezone offset or converted to UTC, I just join this view and choose to get the date in a date format. Instead of datetime, I converted them to datetimeoffset.
select createdon, dst.zone , case when createdon >= dstlow and createdon < dsthigh then dst.daylight else dst.standard end pacificoffsettime , TODATETIMEOFFSET(createdon, case when createdon >= dstlow and createdon < dsthigh then dst.daylight else dst.standard end) pacifictime , SWITCHOFFSET(TODATETIMEOFFSET(createdon, case when createdon >= dstlow and createdon < dsthigh then dst.daylight else dst.standard end), '+00:00') utctime from (select '2014-01-01 12:00:00' createdon union select '2014-06-01 12:00:00' createdon) photos left join US_DAYLIGHT_DATES dst on createdon between yrstart and yrend and zone = 'PT'