2008-08-24 11 views
29

¿Cuál es la mejor manera de convertir una fecha y hora UTC en hora local? No es tan simple como las diferencias getutcdate() y getdate() porque la diferencia cambia según la fecha.Conversión efectiva de fechas entre UTC y hora local (es decir, PST) en SQL 2005

La integración de CLR tampoco es una opción para mí. La solución que había surgido para este problema hace unos meses era tener un horario de ahorro de energía que almacenaba los días de ahorro de luz diurna al principio y al final durante los siguientes 100 años aproximadamente, esta solución parecía poco elegante pero las conversiones fueron rápidos (simple búsqueda de tabla)

Respuesta

26

Cree dos tablas, y luego unirse a ellos para convertir almacena data GMT a la hora local:

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

crear la tabla de horario de verano y rellenarla con toda la información que se puede (leyes locales cambiar todo el tiempo así que no hay manera de predecir lo que los datos se parecerán años en el futuro)

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

unirse a ellos de esta manera:

fechas Convierte como este:

dateadd(hh, tz.Offset + 
    case when ds.LocalTimeZone is not null 
    then 1 else 0 end, TheDateToConvert) 
+6

Debido dateadd acepta sólo números enteros que almacenan el desplazamiento en minutos para hacer frente a las compensaciones, tales como 9,5. – Simon

0

Mantenga una tabla TimeZone o bote con un proceso almacenado extendido (xp_cmdshell o un componente COM, o el suyo propio) y solicite al sistema operativo que lo haga. Si vas a la ruta xp, probablemente quieras almacenar en caché el desplazamiento por un día.

5

Si cualquiera de estos temas le afecta, nunca se debe almacenar horas locales en la base de datos:

  1. Con el horario de verano es que no hay una "hora de incertidumbre" alrededor del período de retroceso en el que una hora local no se puede convertir sin ambigüedad. Si se requieren las fechas exactas &, entonces almacene en UTC.
  2. Si desea mostrar a los usuarios la fecha & vez en su propia zona horaria, en lugar de la zona horaria en la que tuvo lugar la acción, almacenar en UTC.
+0

¿O almacena la hora local y el desplazamiento de la zona horaria? Suponiendo que puede capturar el desplazamiento de zona horaria cuando captura la hora, esto no es ambiguo. Si conoce el desplazamiento de la zona horaria aplicable cuando se capturó la hora, siempre puede convertir a UTC y es útil si desea mostrar un rango de veces en diferentes zonas (en lugar de convertirlo todo a la zona horaria local, por ejemplo). Sigo oyendo que es una buena práctica almacenar en UTC, pero a mí eso es tirar información. –

+0

Bueno, ¿qué pasa con el almacenamiento de la compensación de zona horaria UTC plus? – keuleJ

14

Si está en los EE. UU. Y solo está interesado en pasar de UTC/GMT a un huso horario fijo (como EDT), este código debería ser suficiente. Lo preparé hoy y creo que es correcto, pero utilícelo bajo su propio riesgo.

Agrega una columna calculada a una tabla 'myTable' suponiendo que sus fechas están en la columna 'fecha'. Espero que alguien más encuentre esto útil.

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) 
+0

No fue una respuesta a la pregunta real, pero resolvió mi problema. ¡¡Gracias!! – Bill

+0

Me alegra saber que alguien más lo encontró útil. Tiene razón en que no es una solución para el problema genérico que se planteó, pero debería ser correcta para la mayoría de los lugares en los EE. UU., Que es donde se encuentran todos mis servidores. –

+0

Gracias por compartir este código. Me salvó haciendo las matemáticas de la fecha esta mañana. –

5

En Eric Z Beard's answer, el siguiente 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 

podría ser más exactamente:

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 

(por encima de código no probado)

La razón de esto es que el sql la declaración "entre" es inclusiva. En el back-end de DST, esto daría como resultado un tiempo de 2AM que NO se convertiría a 1AM.Por supuesto, la probabilidad de que el tiempo sea de las 2 AM, precisamente, es pequeña, pero puede suceder y daría como resultado una conversión no válida.

+0

Larry - has mencionado "Respuesta 7". Creo que te estarás refiriendo a la respuesta de Eric Z Beard. El 7 al que te refieres es reputación y es probable que cambie. –

+0

Reemplazado con un enlace a la respuesta referida. –

6

DE SÓLO LECTURA Utilice esta (inspirado por 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 

he publicado esta respuesta después de que trató de editar Bob Albright's wrong answer. Corregí los tiempos y eliminé abs superfluos(), pero mis ediciones fueron rechazadas varias veces. Intenté explicarlo, pero me despidieron como novato. ¡Es un GRAN enfoque al problema! Me hizo comenzar en la dirección correcta. Odio crear esta respuesta por separado cuando solo necesita un pequeño ajuste, pero lo intenté ¯ \ _ (ツ) _/¯

+0

Gracias por compartir Will. Por mi parte, puedo reconocer que, por el estilo de esta respuesta, CLARAMENTE no eres un novato. – pimbrouwers

7

Una solución mucho más simple y genérica que considera el horario de verano. Dada una fecha de UTC en "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] 
+5

No estoy seguro de que este código sea correcto. Obtiene el tiempo diferente de este momento (incluido el horario de verano) y lo aplica a YourDate, que puede o no tener el mismo horario de verano en este momento. – Sheepy

+0

He actualizado el script de arriba. El "YourUtcDateHere" es para un tipo de datos DateTime que no tiene aplicado un desplazamiento UTC. – MikeTeeVee

+1

Tenga en cuenta que esta solución se basa en la zona horaria del servidor donde está instalado SQL Server, que puede no ser igual a la zona horaria del usuario o la aplicación. – Dinei

0

me gusta la respuesta @Eric Z Beard proporcionado.

Sin embargo, para evitar realizar una unión cada vez, ¿qué tal esto?

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 

Entonces

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) 

finalmente convertirse en

dateadd(hh, @offset, TheDateToConvert) 
0

He leído a través de una gran cantidad de mensajes StackOverflow en lo que respecta a este tema y encontré muchos métodos. Algún "tipo de" ok. También encontré esta referencia de MS (https://msdn.microsoft.com/en-us/library/mt612795.aspx) que traté de utilizar en mi script. Logré lograr el resultado requerido PERO no estoy seguro si esto funcionará en la versión 2005. De cualquier manera, espero que esto ayude.

Fnc para volver PST de la UTC por defecto del sistema

CREATE FUNCTION dbo.GetPst() 
RETURNS DATETIME 
AS 
BEGIN 

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

END 

SELECT dbo.GetPst() 

Fnc para volver PST de la marca de tiempo

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') 
+0

Según su enlace 'AT TIME ZONE' es solo una característica de SQL 2016+ –

0

estoy usando esto porque todas las fechas son a partir de ahora hacia adelante.

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

Para las fechas históricas (o para manejar los cambios futuros en el horario de verano, que supongo solución de Bob Albright sería el camino a seguir.

La modificación que hago a mi código es el uso de la columna de destino :.

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

hasta el momento, esto parece funcionar, pero estoy feliz de recibir retroalimentación

0

este es el código que utilizo para hacer mi mesa de zona horaria es un poco ingenuo, pero es usua. bastante bien.

Supuestos:

  1. Asume estadounidense únicas reglas (DST es 02 a.m. en algunas Domingo predefinido, etc).
  2. Se supone que no tiene fechas anteriores a 1970
  3. Se supone que usted sabe las compensaciones de zona horaria local (es decir: EST = -05: 00, EDT = -04: 00, etc.)

Aquí está el 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 
Cuestiones relacionadas