Effectively Converting dates between UTC and Local (ie. PST) time in SQL 2005
Asked Answered
S

13

30

What is the best way to convert a UTC datetime into local datetime. It isn't as simple as a getutcdate() and getdate() difference because the difference changes depending on what the date is.

CLR integration isn't an option for me either.

The solution that I had come up with for this problem a few months back was to have a daylight savings time table that stored the beginning and ending daylight savings days for the next 100 or so years, this solution seemed inelegant but conversions were quick (simple table lookup)

Sinful answered 24/8, 2008 at 2:8 Comment(0)
I
29

Create two tables and then join to them to convert stored GMT dates to local time:

TimeZones     e.g.
---------     ----
TimeZoneId    19
Name          Eastern (GMT -5)
Offset        -5

Create the daylight savings table and populate it with as much information as you can (local laws change all the time so there's no way to predict what the data will look like years in the future)

DaylightSavings
---------------
TimeZoneId    19
BeginDst      3/9/2008 2:00 AM
EndDst        11/2/2008 2:00 AM

Join them like this:

inner join  TimeZones       tz on x.TimeZoneId=tz.TimeZoneId
left join   DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone 
    and x.TheDateToConvert between ds.BeginDst and ds.EndDst

Convert dates like this:

dateadd(hh, tz.Offset + 
    case when ds.LocalTimeZone is not null 
    then 1 else 0 end, TheDateToConvert)
Investigation answered 24/8, 2008 at 14:17 Comment(1)
Because dateadd accepts whole numbers only I stored the offset in minutes to deal with offsets such as 9.5.Skillful
S
15

If you're in the US and only interested in going from UTC/GMT to a fixed time zone (such as EDT) this code should suffice. I whipped it up today and believe it's correct but use at your own risk.

Adds a computed column to a table 'myTable' assuming your dates are on the 'date' column. Hope someone else finds this useful.

ALTER TABLE myTable ADD date_edt AS 
  dateadd(hh, 
        -- The schedule through 2006 in the United States was that DST began on the first Sunday in April 
        -- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006). 
        -- The time is adjusted at 02:00 local time.
              CASE WHEN YEAR(date) <= 2006 THEN  
                    CASE WHEN 
                              date >=  '4/' + CAST(abs(8-DATEPART(dw,'4/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) +  '/' + CAST(YEAR(date) as varchar) + ' 2:00' 
                          AND 
                              date < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date) as varchar)) as varchar) +  '/' + CAST(YEAR(date) as varchar) + ' 2:00' 
                    THEN -4 ELSE -5 END
              ELSE
        -- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007. 
        -- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on 
        -- the first Sunday of November, one week later than in years past. This change resulted in a new DST period 
        -- that is four weeks (five in years when March has five Sundays) longer than in previous years.[35] In 2008 
        -- daylight saving time ended at 02:00 on Sunday, November 2, and in 2009 it began at 02:00 on Sunday, March 8.[36]
                    CASE WHEN 
                              date >= '3/' + CAST(abs(8-DATEPART(dw,'3/1/' + CAST(YEAR(date) as varchar)))%7 + 8 as varchar) +  '/' + CAST(YEAR(date) as varchar) + ' 2:00' 
                          AND 
                              date < 
                                '11/' + CAST(abs(8-DATEPART(dw,'11/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) +  '/' + CAST(YEAR(date) as varchar) + ' 2:00' 
                    THEN -4 ELSE -5 END
              END
  ,date)
Scarper answered 8/5, 2009 at 4:1 Comment(5)
Not an answer to the actual question, but it solved my issue. Thanks!!Culhert
Glad to hear that someone else found it useful! You're right that it's not a solution for the generic problem that was posed but it should be correct for most places in the US which is where all of my servers happen to be located.Scarper
Thank you for sharing this code. It saved me doing out the date math this morning.Recursion
Apologies for swooping in many years later, but... it looks to me as if you have the transitions happening at 0200Z, but they happen at 0100EST/0200EDT, which is 0600Z. Otherwise looks pretty good to me, am using it to fix up my lack of foresight mixing local and UTC stamps in my database. At least my servers and users are all in one TZ.Iey
@Iey yep, that's mostly why I added answer: https://mcmap.net/q/160381/-effectively-converting-dates-between-utc-and-local-ie-pst-time-in-sql-2005Pliam
P
9

FOR READ-ONLY Use this(inspired by Bob Albright's incorrect solution ):

SELECT
  date1, 
  dateadd(hh,
    -- The schedule through 2006 in the United States was that DST began on the first Sunday in April 
    -- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006). 
    -- The time is adjusted at 02:00 local time (which, for edt, is 07:00 UTC at the start, and 06:00 GMT at the end).
    CASE WHEN YEAR(date1) <= 2006 THEN
         CASE WHEN 
                  date1 >=  '4/' + CAST((8-DATEPART(dw,'4/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) +  '/' + CAST(YEAR(date1) as varchar) + ' 7:00' 
                AND 
                  date1 < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date1) as varchar)) as varchar) +  '/' + CAST(YEAR(date1) as varchar) + ' 6:00' 
              THEN -4 ELSE -5 END
    ELSE
        -- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007. 
        -- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on 
        -- the first Sunday of November, one week later than in years past. This change resulted in a new DST period 
        -- that is four weeks (five in years when March has five Sundays) longer than in previous years. In 2008 
        -- daylight saving time ended at 02:00 edt (06:00 UTC) on Sunday, November 2, and in 2009 it began at 02:00 edt (07:00 UTC) on Sunday, March 8
        CASE WHEN 
                 date1 >= '3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(date1) as varchar)))%7 + 8 as varchar) +  '/' + CAST(YEAR(date1) as varchar) + ' 7:00' 
               AND 
                 date1 < '11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) +  '/' + CAST(YEAR(date1) as varchar) + ' 6:00' 
             THEN -4 ELSE -5 END
    END
   , date1) as date1Edt
  from MyTbl

I posted this answer after I tried to edit Bob Albright's wrong answer. I corrected the times and removed superfluous abs(), but my edits were rejected multiple times. I tried explaining, but was dismissed as a noob. His is a GREAT approach to the problem! It got me started in the right direction. I hate to create this separate answer when his just needs a minor tweak, but I tried ¯\_(ツ)_/¯

Pliam answered 7/6, 2013 at 23:44 Comment(1)
Thanks for sharing Will. I for one can acknowledge that by the ilk of this answer you are CLEARLY not a noob.Amice
Q
7

A much simpler and generic solution that considers daylight savings. Given an UTC date in "YourDateHere":

--Use Minutes ("MI") here instead of hours because sometimes
--  the UTC offset may be half an hour (e.g. 9.5 hours).
SELECT DATEADD(MI,
               DATEDIFF(MI, SYSUTCDATETIME(),SYSDATETIME()),
               YourUtcDateHere)[LocalDateTime]
Quantify answered 23/7, 2013 at 20:42 Comment(4)
Not sure this code is right. It get the time different of this moment (including DST) and apply it to YourDate, which may or may not have same DST with this moment.Comptom
I updated the script above. The "YourUtcDateHere" is for a DateTime datatype that has no UTC offset applied to it.Hipster
Be aware that this solution relies on the timezone of the server where the SQL Server is installed, which may not be the same as the user's or application's timezone.Seignior
Sheepy is right. The answer doesn't accommodate Daylight Saving - which OP mentioned. @Quantify & MikeTeevee answer only works in two cases: 1) if run during DST and the queried dateTime is during DST ...or 2) executed outside DST and the queried dateTime is outside DST. That is, it will be wrong in the other two cases: 1) the query is run during DST and the queried date is NOT during DST and 2) the "current time" is NOT during DST and the queried dateTime is during DST - see my solution to account for this https://mcmap.net/q/160381/-effectively-converting-dates-between-utc-and-local-ie-pst-time-in-sql-2005 ...which is specific to Eastern TimePliam
O
5

If either of these issues affects you, you should never store local times in the database:

  1. With DST is that there is an "hour of uncertainty" around the falling back period where a local time cannot be unambiguously converted. If exact dates & times are required, then store in UTC.
  2. If you want to show users the date & time in their own timezone, rather than the timezone in which the action took place, store in UTC.
Oddment answered 24/8, 2008 at 20:45 Comment(2)
Or store local time and the time zone offset? Assuming you can capture the time zone offset when you capture the time, this is unambiguous. If you know the time zone offset applicable when the time was captured you can always convert to UTC and is useful if you want to show a range of times across different zones (rather than converting all to the local time zone for example). I keep hearing that it's best practice to store in UTC but to me that's throwing information away.Seldun
Well what about storing UTC plus time zone offset?Conjugated
P
5

In Eric Z Beard's answer, the following SQL

inner join  TimeZones       tz on x.TimeZoneId=tz.TimeZoneId 
left join   DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone  
    and x.TheDateToConvert between ds.BeginDst and ds.EndDst 

might more accurately be:

inner join  TimeZones       tz on x.TimeZoneId=tz.TimeZoneId 
left join   DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone  
    and x.TheDateToConvert >= ds.BeginDst and x.TheDateToConvert < ds.EndDst 

(above code not tested)

The reason for this is that the sql "between" statement is inclusive. On the back-end of DST, this would result in a 2AM time NOT being converted to 1AM. Of course the likelihood of the time being 2AM precisely is small, but it can happen, and it would result in an invalid conversion.

Privilege answered 27/10, 2010 at 16:55 Comment(2)
Larry - you've referred to "Answer 7". I think you might be referring to Eric Z Beard's answer? The 7 that you refer to is reputation and is likely to change.Seldun
Replaced with link to the referred-to answer.Midriff
M
0

Maintain a TimeZone table, or shell out with an extended stored proc (xp_cmdshell or a COM component, or your own) and ask the OS to do it. If you go the xp route, you'd probably want to cache the offset for a day.

Miss answered 24/8, 2008 at 13:28 Comment(0)
V
0

I like the answer @Eric Z Beard provided.

However, to avoid performing a join everytime, what about this?

TimeZoneOffsets
---------------
TimeZoneId    19
Begin         1/4/2008 2:00 AM
End           1/9/2008 2:00 AM
Offset        -5
TimeZoneId    19
Begin         1/9/2008 2:00 AM
End           1/4/2009 2:00 AM
Offset        -6
TimeZoneId    20 --Hong Kong for example - no DST
Begin         1/1/1900
End           31/12/9999
Offset        +8

Then

 Declare @offset INT = (Select IsNull(tz.Offset,0) from YourTable ds
 join   TimeZoneOffsets tz on tz.TimeZoneId=ds.LocalTimeZoneId  
 and x.TheDateToConvert >= ds.Begin and x.TheDateToConvert < ds.End)

finally becoming

 dateadd(hh, @offset, TheDateToConvert)
Vivianna answered 3/4, 2014 at 15:48 Comment(0)
W
0

I've read through a lot of StackOverflow posts in regards to this issue and found many methods. Some "sort of" ok. I also found this MS reference (https://msdn.microsoft.com/en-us/library/mt612795.aspx) which I tried to utilize in my script. I have managed to achieve the required result BUT I am not sure if this will run on 2005 version. Either way, I hope this helps.

Fnc to return PST from the system UTC default

CREATE FUNCTION dbo.GetPst()
RETURNS DATETIME
AS 
BEGIN

    RETURN  SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time'

END

SELECT dbo.GetPst()

Fnc to return PST from the provided timestamp

CREATE FUNCTION dbo.ConvertUtcToPst(@utcTime DATETIME)
RETURNS DATETIME
AS
BEGIN

    RETURN DATEADD(HOUR, 0 - DATEDIFF(HOUR, CAST(SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time' AS DATETIME), SYSDATETIME()), @utcTime)

END


SELECT dbo.ConvertUtcToPst('2016-04-25 22:50:01.900')
Whitebook answered 26/4, 2016 at 15:36 Comment(2)
Per your link AT TIME ZONE is a SQL 2016+ feature onlyCadwell
these answers that DIFF current local time and UTC are right SOMETIMES, but not always - depending on WHEN executed! The diff that should apply to your DB records depends on whether it's during DST or not - which may differ from you current DST status when executing: [#25297Pliam
M
0

I am using this because all of my dates are from now forward.

DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, GETDATE())

For historical dates (or to handle future changes in DST, I'm guessing Bob Albright's solution would be the way to go.

The modification I make to my code is to use the target column:

DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, [MySourceColumn])

So far, this seems to work, but I'm happy to receive feedback.

Metalinguistics answered 15/8, 2016 at 18:20 Comment(0)
D
0

Here is the code I use to make my timezone table. It's a bit naive, but is usually good enough.

Assumptions:

  1. It assumes US only rules (DST is 2AM on some pre-defined Sunday, etc).
  2. It assumes you don't have dates prior to 1970
  3. It assumes you know the local timezone offsets (i.e.: EST=-05:00, EDT=-04:00, etc.)

Here's the SQL:

-- make a table (#dst) of years 1970-2101. Note that DST could change in the future and
-- everything was all custom and jacked before 1970 in the US.
declare @first_year varchar(4) = '1970'
declare @last_year varchar(4) = '2101'

-- make a table of all the years desired
if object_id('tempdb..#years') is not null drop table #years
;with cte as (
    select cast(@first_year as int) as int_year
          ,@first_year as str_year
          ,cast(@first_year + '-01-01' as datetime) as start_of_year
    union all
    select int_year + 1
          ,cast(int_year + 1 as varchar(4))
          ,dateadd(year, 1, start_of_year)
    from cte
    where int_year + 1 <= @last_year
)
select *
into #years
from cte
option (maxrecursion 500);

-- make a staging table of all the important DST dates each year
if object_id('tempdb..#dst_stage') is not null drop table #dst_stage
select dst_date
      ,time_period
      ,int_year
      ,row_number() over (order by dst_date) as ordinal
into #dst_stage
from (
    -- start of year
    select y.start_of_year as dst_date
          ,'start of year' as time_period
          ,int_year
    from #years y

    union all
    select dateadd(year, 1, y.start_of_year)
          ,'start of year' as time_period
          ,int_year
    from #years y
    where y.str_year = @last_year

    -- start of dst
    union all
    select
        case
            when y.int_year >= 2007 then
                -- second sunday in march
                dateadd(day, ((7 - datepart(weekday, y.str_year + '-03-08')) + 1) % 7, y.str_year + '-03-08')
            when y.int_year between 1987 and 2006 then
                -- first sunday in april
                dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-01')) + 1) % 7, y.str_year + '-04-01')
            when y.int_year = 1974 then
                -- special case
                cast('1974-01-06' as datetime)
            when y.int_year = 1975 then
                -- special case
                cast('1975-02-23' as datetime)
            else
                -- last sunday in april
                dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-24')) + 1) % 7, y.str_year + '-04-24')
        end
        ,'start of dst' as time_period
        ,int_year
    from #years y

    -- end of dst
    union all
    select
        case
            when y.int_year >= 2007 then
                -- first sunday in november
                dateadd(day, ((7 - datepart(weekday, y.str_year + '-11-01')) + 1) % 7, y.str_year + '-11-01')
            else
                -- last sunday in october
                dateadd(day, ((7 - datepart(weekday, y.str_year + '-10-25')) + 1) % 7, y.str_year + '-10-25')
        end
        ,'end of dst' as time_period
        ,int_year
    from #years y
) y
order by 1

-- assemble a final table
if object_id('tempdb..#dst') is not null drop table #dst
select a.dst_date +
          case
             when a.time_period = 'start of dst' then ' 03:00'
             when a.time_period = 'end of dst' then ' 02:00'
             else ' 00:00'
          end as start_date
      ,b.dst_date +
          case
             when b.time_period = 'start of dst' then ' 02:00'
             when b.time_period = 'end of dst' then ' 01:00'
             else ' 00:00'
          end as end_date
      ,cast(case when a.time_period = 'start of dst' then 1 else 0 end as bit) as is_dst
      ,cast(0 as bit) as is_ambiguous
      ,cast(0 as bit) as is_invalid
into #dst
from #dst_stage a
join #dst_stage b on a.ordinal + 1 = b.ordinal
union all
select a.dst_date + ' 02:00' as start_date
      ,a.dst_date + ' 03:00' as end_date
      ,cast(1 as bit) as is_dst
      ,cast(0 as bit) as is_ambiguous
      ,cast(1 as bit) as is_invalid
from #dst_stage a
where a.time_period = 'start of dst'
union all
select a.dst_date + ' 01:00' as start_date
      ,a.dst_date + ' 02:00' as end_date
      ,cast(0 as bit) as is_dst
      ,cast(1 as bit) as is_ambiguous
      ,cast(0 as bit) as is_invalid
from #dst_stage a
where a.time_period = 'end of dst'
order by 1

-------------------------------------------------------------------------------

-- Test Eastern
select
    the_date as eastern_local
    ,todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end) as eastern_local_tz
    ,switchoffset(todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end), '+00:00') as utc_tz
    --,b.*
from (
    select cast('2015-03-08' as datetime) as the_date
    union all select cast('2015-03-08 02:30' as datetime) as the_date
    union all select cast('2015-03-08 13:00' as datetime) as the_date
    union all select cast('2015-11-01 01:30' as datetime) as the_date
    union all select cast('2015-11-01 03:00' as datetime) as the_date
) a left join
#dst b on b.start_date <= a.the_date and a.the_date < b.end_date
Deplore answered 28/1, 2017 at 0:59 Comment(0)
C
0
--Adapted Bob Albright and WillDeStijl suggestions for SQL server 2014
--
--In this instance I had no dates prior to 2006, therefore I simplified the case example
--I had to add the variables for the assignment to allow trimming the timestamp from my resultset 

DECLARE @MARCH_DST as DATETIME
SET @MARCH_DST='3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 8 as varchar) +  '/' + CAST(YEAR(getdate()) as varchar) + ' 7:00'

DECLARE @NOV_DST as DATETIME
SET @NOV_DST='11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 1 as varchar) +  '/' + CAST(YEAR(getdate()) as varchar) + ' 6:00'

select cast(dateadd(HOUR,
-- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007. 
        -- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on 
        -- the first Sunday of November, one week later than in years past. This change resulted in a new DST period 
        -- that is four weeks (five in years when March has five Sundays) longer than in previous years. In 2008 
        -- daylight saving time ended at 02:00 edt (06:00 UTC) on Sunday, November 2, and in 2009 it began at 02:00 edt (07:00 UTC) on Sunday, March 8
       CASE WHEN
                date1 >=@MARCH_DST
            AND
                date1< @NOV_DST
       THEN -4 ELSE -5 END
       , date1) as DATE) as date1_edited

Conrad answered 24/4, 2019 at 4:27 Comment(0)
M
0

I found Simple Way to convert any date to any timezone. Currently i have changed date to India Standard Time


    
DECLARE @SqlServerTimeZone VARCHAR(50)

DECLARE @LocalTimeZone VARCHAR(50)='India Standard Time'

EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE',
'SYSTEM\CurrentControlSet\Control\TimeZoneInformation',
'TimeZoneKeyName',@SqlServerTimeZone OUT


DECLARE @DateToConvert datetime= GetDate()

SELECT LocalDate =  @DateToConvert AT TIME ZONE @SqlServerTimeZone AT TIME ZONE  @LocalTimeZone




Murchison answered 5/12, 2021 at 14:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.