web-dev-qa-db-fra.com

Comment puis-je obtenir le décalage correct entre l'heure UTC et l'heure locale pour une date antérieure ou postérieure à l'heure d'été?

J'utilise actuellement ce qui suit pour obtenir un datetime local à partir d'un datetime UTC:

SET @offset = DateDiff(minute, GetUTCDate(), GetDate())
SET @localDateTime = DateAdd(minute, @offset, @utcDateTime)

Mon problème est que si l'heure d'été se produit entre GetUTCDate() et @utcDateTime, Le @localDateTime Finit par être une heure de repos.

Existe-t-il un moyen simple de convertir l'utc en heure locale pour une date qui n'est pas la date actuelle?

J'utilise SQL Server 2005

30
Rachel

La meilleure façon de convertir une date UTC non actuelle en heure locale est d'utiliser le CLR. Le code lui-même est facile; la partie difficile est généralement de convaincre les gens que le CLR n'est pas du mal pur ou effrayant ...

Pour l'un des nombreux exemples, consultez article de blog de Harsh Chawla sur le sujet .

Malheureusement, rien de intégré ne peut gérer ce type de conversion, à l'exception des solutions basées sur CLR. Vous pouvez écrire une fonction T-SQL qui fait quelque chose comme ça, mais vous devez alors implémenter vous-même la logique de changement de date, et je dirais que ce n'est décidément pas facile.

19
Kevin Feasel

J'ai développé et publié le projet T-SQL Toolbox sur codeplex pour aider tous ceux qui ont des problèmes avec la gestion du datetime et du fuseau horaire dans Microsoft SQL Server. Il est open source et totalement gratuit.

Il offre des UDF de conversion datetime faciles en utilisant du T-SQL simple (pas de CLR) en plus des tableaux de configuration préremplis prêts à l'emploi. Et il a une prise en charge complète de l'heure d'été (DST).

Une liste de tous les fuseaux horaires pris en charge se trouve dans le tableau "DateTimeUtil.Timezone" (fourni dans la base de données T-SQL Toolbox).

Dans votre exemple, vous pouvez utiliser l'exemple suivant:

SELECT [DateTimeUtil].[UDF_ConvertUtcToLocalByTimezoneIdentifier] (
    'W. Europe Standard Time', -- the target local timezone
    '2014-03-30 00:55:00' -- the original UTC datetime you want to convert
)

Cela renverra la valeur datetime locale convertie.

Malheureusement, il est pris en charge pour SQL Server 2008 ou version ultérieure uniquement en raison de nouveaux types de données (DATE, TIME, DATETIME2). Mais comme le code source complet est fourni, vous pouvez facilement ajuster les tables et les FDU en les remplaçant par DATETIME. Je n'ai pas de MSSQL 2005 disponible pour les tests, mais il devrait également fonctionner avec MSSQL 2005. En cas de questions, faites le moi savoir.

15
adss

J'utilise toujours cette commande TSQL.

-- the utc value 
declare @utc datetime = '20/11/2014 05:14'

-- the local time

select DATEADD(hh, DATEDIFF(hh, getutcdate(), getdate()), @utc)

C'est très simple et ça fait l'affaire.

13
Ludo Bernaerts

J'ai trouvé cette réponse sur StackOverflow qui fournit une fonction définie par l'utilisateur qui semble traduire avec précision les heures

La seule chose que vous devez modifier est le @offset variable en haut pour le définir sur le décalage de fuseau horaire du serveur SQL exécutant cette fonction. Dans mon cas, notre serveur SQL utilise EST, qui est GMT - 5

Ce n'est pas parfait et ne fonctionnera probablement pas dans de nombreux cas, comme les décalages TZ d'une demi-heure ou de 15 minutes (pour ceux que je recommanderais une fonction CLR comme Kevin recommandé ), mais cela fonctionne assez bien pour la plupart des fuseaux horaires génériques en Amérique du Nord.

CREATE FUNCTION [dbo].[UDTToLocalTime](@UDT AS DATETIME)  
RETURNS DATETIME
AS
BEGIN 
--====================================================
--Set the Timezone Offset (NOT During DST [Daylight Saving Time])
--====================================================
DECLARE @Offset AS SMALLINT
SET @Offset = -5

--====================================================
--Figure out the Offset Datetime
--====================================================
DECLARE @LocalDate AS DATETIME
SET @LocalDate = DATEADD(hh, @Offset, @UDT)

--====================================================
--Figure out the DST Offset for the UDT Datetime
--====================================================
DECLARE @DaylightSavingOffset AS SMALLINT
DECLARE @Year as SMALLINT
DECLARE @DSTStartDate AS DATETIME
DECLARE @DSTEndDate AS DATETIME
--Get Year
SET @Year = YEAR(@LocalDate)

--Get First Possible DST StartDay
IF (@Year > 2006) SET @DSTStartDate = CAST(@Year AS CHAR(4)) + '-03-08 02:00:00'
ELSE              SET @DSTStartDate = CAST(@Year AS CHAR(4)) + '-04-01 02:00:00'
--Get DST StartDate 
WHILE (DATENAME(dw, @DSTStartDate) <> 'sunday') SET @DSTStartDate = DATEADD(day, 1,@DSTStartDate)


--Get First Possible DST EndDate
IF (@Year > 2006) SET @DSTEndDate = CAST(@Year AS CHAR(4)) + '-11-01 02:00:00'
ELSE              SET @DSTEndDate = CAST(@Year AS CHAR(4)) + '-10-25 02:00:00'
--Get DST EndDate 
WHILE (DATENAME(dw, @DSTEndDate) <> 'sunday') SET @DSTEndDate = DATEADD(day,1,@DSTEndDate)

--Get DaylightSavingOffset
SET @DaylightSavingOffset = CASE WHEN @LocalDate BETWEEN @DSTStartDate AND @DSTEndDate THEN 1 ELSE 0 END

--====================================================
--Finally add the DST Offset 
--====================================================
RETURN DATEADD(hh, @DaylightSavingOffset, @LocalDate)
END



GO
11
Rachel

Pour SQL Server 2016+, vous pouvez utiliser AT TIME ZONE . Il gérera automatiquement l'heure d'été.

5
gotqn

Il y a quelques bonnes réponses à une question similaire posée sur Stack Overflow. J'ai fini par utiliser une approche T-SQL de la deuxième réponse de Bob Albright pour nettoyer un gâchis causé par un consultant en conversion de données.

Cela a fonctionné pour presque toutes nos données, mais j'ai ensuite réalisé que son algorithme ne fonctionnait que pour des dates remontant au 5 avril 1987 , et nous avait des dates des années 40 qui ne se convertissaient toujours pas correctement. Nous avions finalement besoin des dates UTC dans notre base de données SQL Server pour nous aligner avec un algorithme dans un programme tiers qui utilisait l'API Java pour convertir de UTC en heure locale .

J'aime l'exemple CLRdans la réponse de Kevin Feasel ci-dessus en utilisant l'exemple de Harsh Chawla, et je voudrais également le comparer à une solution qui utilise Java, puisque notre frontal utilise Java pour effectuer la conversion de UTC en heure locale.

Wikipédia mentionne 8 modifications constitutionnelles différentes qui impliquent des ajustements de fuseau horaire avant 1987, et bon nombre d'entre elles sont très localisées dans différents États, il est donc possible que le CLR et Java les interprètent différemment. Votre code d'application frontal utilise-t-il dotnet ou Java, ou les dates antérieures à 1987 sont-elles un problème pour vous?

3
kkarns
DECLARE @TimeZone VARCHAR(50)
EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE', 'SYSTEM\CurrentControlSet\Control\TimeZoneInformation', 'TimeZoneKeyName', @TimeZone OUT
SELECT @TimeZone
DECLARE @someUtcTime DATETIME
SET @someUtcTime = '2017-03-05 15:15:15'
DECLARE @TimeBiasAtSomeUtcTime INT
SELECT @TimeBiasAtSomeUtcTime = DATEDIFF(MINUTE, @someUtcTime, @someUtcTime AT TIME ZONE @TimeZone)
SELECT DATEADD(MINUTE, @TimeBiasAtSomeUtcTime * -1, @someUtcTime)
2
Joost Versteegen

Vous pouvez facilement le faire avec une procédure stockée CLR.

[SqlFunction]
public static SqlDateTime ToLocalTime(SqlDateTime UtcTime, SqlString TimeZoneId)
{
    if (UtcTime.IsNull)
        return UtcTime;

    var timeZone = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId.Value);
    var localTime = TimeZoneInfo.ConvertTimeFromUtc(UtcTime.Value, timeZone);
    return new SqlDateTime(localTime);
}

Vous pouvez stocker les TimeZones disponibles dans une table:

CREATE TABLE TimeZones
(
    TimeZoneId NVARCHAR(32) NOT NULL CONSTRAINT PK_TimeZones PRIMARY KEY,
    DisplayName NVARCHAR(64) NOT NULL,
    SupportsDaylightSavingTime BIT NOT NULL,
)

Et cette procédure stockée remplira le tableau avec les fuseaux horaires possibles sur votre serveur.

public partial class StoredProcedures
{
    [SqlProcedure]
    public static void PopulateTimezones()
    {
        using (var sql = new SqlConnection("Context Connection=True"))
        {
            sql.Open();

            using (var cmd = sql.CreateCommand())
            {
                cmd.CommandText = "DELETE FROM TimeZones";
                cmd.ExecuteNonQuery();

                cmd.CommandText = "INSERT INTO [dbo].[TimeZones]([TimeZoneId], [DisplayName], [SupportsDaylightSavingTime]) VALUES(@TimeZoneId, @DisplayName, @SupportsDaylightSavingTime);";
                var Id = cmd.Parameters.Add("@TimeZoneId", SqlDbType.NVarChar);
                var DisplayName = cmd.Parameters.Add("@DisplayName", SqlDbType.NVarChar);
                var SupportsDaylightSavingTime = cmd.Parameters.Add("@SupportsDaylightSavingTime", SqlDbType.Bit);

                foreach (var zone in TimeZoneInfo.GetSystemTimeZones())
                {
                    Id.Value = zone.Id;
                    DisplayName.Value = zone.DisplayName;
                    SupportsDaylightSavingTime.Value = zone.SupportsDaylightSavingTime;

                    cmd.ExecuteNonQuery();
                }
            }
        }
    }
}
2
Tim Cooke

Voici une réponse écrite pour une application spécifique au Royaume-Uni et basée uniquement sur SELECT.

  1. Aucun décalage de fuseau horaire (par exemple au Royaume-Uni)
  2. Écrit pour l'heure d'été à partir du dernier dimanche de mars et se terminant le dernier dimanche d'octobre (règles britanniques)
  3. Non applicable entre minuit et 1 h du matin le début de l'heure d'été. Cela pourrait être corrigé mais l'application pour laquelle il a été écrit ne l'exige pas.

    -- A variable holding an example UTC datetime in the UK, try some different values:
    DECLARE
    @App_Date datetime;
    set @App_Date = '20250704 09:00:00'
    
    -- Outputting the local datetime in the UK, allowing for daylight saving:
    SELECT
    case
    when @App_Date >= dateadd(day, 1 - datepart(weekday, dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @App_Date), 0)))), dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @App_Date), 0))))
        and @App_Date < dateadd(day, 1 - datepart(weekday, dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @App_Date), 0)))), dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @App_Date), 0))))
        then DATEADD(hour, 1, @App_Date) 
    else @App_Date 
    end
    
2
colinp_1

SQL Server version 2016 résoudra ce problème ne fois pour toutes . Pour les versions antérieures, une solution CLR est probablement la plus simple. Ou pour une règle DST spécifique (comme aux États-Unis uniquement), une fonction T-SQL peut être relativement simple.

Cependant, je pense qu'une solution générique T-SQL pourrait être possible. Aussi longtemps que xp_regread fonctionne, essayez ceci:

CREATE TABLE #tztable (Value varchar(50), Data binary(56));
DECLARE @tzname varchar(150) = 'SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
EXEC master.dbo.xp_regread 'HKEY_LOCAL_MACHINE', @tzname, 'TimeZoneKeyName', @tzname OUT;
SELECT @tzname = 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\' + @tzname
INSERT INTO #tztable
EXEC master.dbo.xp_regread 'HKEY_LOCAL_MACHINE', @tzname, 'TZI';
SELECT                                                                                  -- See http://msdn.Microsoft.com/ms725481
 CAST(CAST(REVERSE(SUBSTRING(Data,  1, 4)) AS binary(4))      AS int) AS BiasMinutes,   -- UTC = local + bias: > 0 in US, < 0 in Europe!
 CAST(CAST(REVERSE(SUBSTRING(Data,  5, 4)) AS binary(4))      AS int) AS ExtraBias_Std, --   0 for most timezones
 CAST(CAST(REVERSE(SUBSTRING(Data,  9, 4)) AS binary(4))      AS int) AS ExtraBias_DST, -- -60 for most timezones: DST makes UTC 1 hour earlier
 -- When DST ends:
 CAST(CAST(REVERSE(SUBSTRING(Data, 13, 2)) AS binary(2)) AS smallint) AS StdYear,       -- 0 = yearly (else once)
 CAST(CAST(REVERSE(SUBSTRING(Data, 15, 2)) AS binary(2)) AS smallint) AS StdMonth,      -- 0 = no DST
 CAST(CAST(REVERSE(SUBSTRING(Data, 17, 2)) AS binary(2)) AS smallint) AS StdDayOfWeek,  -- 0 = Sunday to 6 = Saturday
 CAST(CAST(REVERSE(SUBSTRING(Data, 19, 2)) AS binary(2)) AS smallint) AS StdWeek,       -- 1 to 4, or 5 = last <DayOfWeek> of <Month>
 CAST(CAST(REVERSE(SUBSTRING(Data, 21, 2)) AS binary(2)) AS smallint) AS StdHour,       -- Local time
 CAST(CAST(REVERSE(SUBSTRING(Data, 23, 2)) AS binary(2)) AS smallint) AS StdMinute,
 CAST(CAST(REVERSE(SUBSTRING(Data, 25, 2)) AS binary(2)) AS smallint) AS StdSecond,
 CAST(CAST(REVERSE(SUBSTRING(Data, 27, 2)) AS binary(2)) AS smallint) AS StdMillisec,
 -- When DST starts:
 CAST(CAST(REVERSE(SUBSTRING(Data, 29, 2)) AS binary(2)) AS smallint) AS DSTYear,       -- See above
 CAST(CAST(REVERSE(SUBSTRING(Data, 31, 2)) AS binary(2)) AS smallint) AS DSTMonth,
 CAST(CAST(REVERSE(SUBSTRING(Data, 33, 2)) AS binary(2)) AS smallint) AS DSTDayOfWeek,
 CAST(CAST(REVERSE(SUBSTRING(Data, 35, 2)) AS binary(2)) AS smallint) AS DSTWeek,
 CAST(CAST(REVERSE(SUBSTRING(Data, 37, 2)) AS binary(2)) AS smallint) AS DSTHour,
 CAST(CAST(REVERSE(SUBSTRING(Data, 39, 2)) AS binary(2)) AS smallint) AS DSTMinute,
 CAST(CAST(REVERSE(SUBSTRING(Data, 41, 2)) AS binary(2)) AS smallint) AS DSTSecond,
 CAST(CAST(REVERSE(SUBSTRING(Data, 43, 2)) AS binary(2)) AS smallint) AS DSTMillisec
FROM #tztable;
DROP TABLE #tztable

Une fonction T-SQL (complexe) pourrait utiliser ces données pour déterminer le décalage exact pour toutes les dates pendant la règle DST actuelle.

2
Michel de Ruiter

Basé sur Colinp_1 post J'ai créé une solution pour convertir un datetime en datetimeoffset qui prend en compte DST et TZ. J'espère que cela t'aides!

DECLARE @offset int -- offset in min
DECLARE @dst bit
DECLARE @appDate datetime

set @dst = 1
set @offset = +60
set @appDate = '2017-04-06 14:21:10.000'

-- output the start and end datetime of DST to the given @appDate
select dateadd(hour, 2, 
                  dateadd(day, 1 - datepart(weekday
                                 , dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @appDate), 0))))
                                 , dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @appDate), 0))))) AS 'MEZ -> MESZ'
     , dateadd(hour, 2, 
                  dateadd(day, 1 - datepart(weekday
                                 , dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @appDate), 0))))
                                 , dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @appDate), 0))))) AS 'MESZ -> MEZ'

-- output the @appDate as datetimeoffset including offset and DST
SELECT @dst AS 'DST on'
     , @offset AS 'TZ offset'
     , @appDate AS 'originalDate'
     , qDT.isAppDateInDST
     , qDT.datetimeoffset
     , CONVERT(datetime, qDT.datetimeoffset, 1) AS 'UTC'
FROM (
    SELECT 
        CASE WHEN @dst = 1 THEN -- check if DST is needed
           CASE
                WHEN qDST.isAppDateInDST = 1
                THEN TODATETIMEOFFSET(@appDate, @offset + 60) -- add 1 hour to @appDate when its in DST and convert to DATETIMEOFFSET
                ELSE TODATETIMEOFFSET(@appDate, @offset) -- convert to     DATETIMEOFFSET with given offset
        END
    ELSE 
        TODATETIMEOFFSET(@appDate, @offset) -- convert to DATETIMEOFFSET with given offset
        END AS 'datetimeoffset'
      , qDST.isAppDateInDST
    FROM (
        SELECT 
            CASE WHEN @appDate >= dateadd(hour, 2, 
                                    dateadd(day, 1 - datepart(weekday
                                                    , dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @appDate), 0))))
                                                    , dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @appDate), 0)))))
                    and @appDate < dateadd(hour, 2, 
                                    dateadd(day, 1 - datepart(weekday
                                                    , dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @appDate), 0))))
                                                    , dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @appDate), 0)))))
                THEN 1
            ELSE 0
            END AS 'isAppDateInDST'
    ) qDST
) qDT

GO
0
Mike