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
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.
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.
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.
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
Pour SQL Server 2016+, vous pouvez utiliser AT TIME ZONE . Il gérera automatiquement l'heure d'été.
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 CLR
dans 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?
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)
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();
}
}
}
}
}
Voici une réponse écrite pour une application spécifique au Royaume-Uni et basée uniquement sur SELECT.
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
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.
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