it-swarm-eu.dev

Jak mohu získat správný offset mezi UTC a místními časy pro datum, které je před nebo po DST?

Aktuálně používám následující k získání místního datetime z datového času UTC:

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

Můj problém je, že pokud nastane letní čas mezi GetUTCDate() a @utcDateTime, @localDateTime Končí hodinovou volno.

Existuje snadný způsob převodu z utc na místní čas na datum, které není aktuální datum?

Používám SQL Server 2005

30
Rachel

Nejlepší způsob, jak převést neaktuální datum UTC na místní čas, je použít CLR. Samotný kód je snadný; obtížná část obvykle lidi přesvědčí, že CLR není čistě zlé nebo děsivé ...

Pro jeden z mnoha příkladů, podívejte se na Harsh Chawla blogu na téma .

Bohužel není vestavěné nic, co by zvládlo tento typ převodu, s výjimkou řešení založených na CLR. Mohli byste napsat funkci T-SQL, která něco takového dělá, ale pak byste museli implementovat logiku změny data sami a já bych to rozhodně neřekl snadno.

19
Kevin Feasel

Vyvinul jsem a publikoval projekt T-SQL Toolbox na codeplexu, abych pomohl každému, kdo bojuje s datetime a timezone handlingem na Microsoft SQL Serveru. Je to open source a zcela zdarma k použití.

Nabízí snadné konverze datových časů UDF s použitím prostého T-SQL (bez CLR) a navíc s předem vyplněnými konfiguračními tabulkami po vybalení z krabice. A má plnou podporu DST (letní čas).

Seznam všech podporovaných časových pásem je uveden v tabulce „DateTimeUtil.Timezone“ (poskytované v databázi nástrojů T-SQL Toolbox).

V příkladu můžete použít následující ukázku:

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
)

Tím se vrátí převedená hodnota místní datetime.

Bohužel je podporován pro SQL Server 2008 nebo novější pouze z důvodu novějších typů dat (DATE, TIME, DATETIME2). Protože je však k dispozici úplný zdrojový kód, můžete tabulky a UDF snadno upravit tak, že je nahradíte DATETIME. Nemám k dispozici MSSQL 2005 pro testování, ale měl by také fungovat s MSSQL 2005. V případě dotazů, dejte mi vědět.

15
adss

Tento příkaz TSQL vždy používám.

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

-- the local time

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

Je to velmi jednoduché a dělá svou práci.

13
Ludo Bernaerts

Našel jsem tato odpověď na StackOverflow, který poskytuje User Defined Function, která vypadá, že přesně překládá datové časy

Jediné, co musíte upravit, je @offset proměnná nahoře, aby byla nastavena na offset časových pásem serveru SQL, na kterém je tato funkce spuštěna. V mém případě náš SQL server používá EST, což je GMT - 5

Není to dokonalé a pravděpodobně nebude fungovat v mnoha případech, jako je například půlhodinová nebo 15minutová korekce TZ (pro ty, které bych doporučil funkci CLR jako doporučeno Kevin ), ale funguje to dost dobře pro většinu obecných časových pásem v Severní Americe.

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

Pro SQL Server 2016+ můžete použít AT TIME ZONE . Bude automaticky zpracovávat denní čas.

5
gotqn

Existuje několik dobrých odpovědí na otázku podobná otázka na Stack Overflow. Skončil jsem pomocí přístupu T-SQL od druhá odpověď Bob Albright , abych vyčistil nepořádek způsobený konzultantem pro převod dat.

Fungovalo to téměř pro všechna naše data, ale později jsem si uvědomil, že jeho algoritmus funguje pouze pro data až do 5. dubna 1987 a my měl data ze čtyřicátých let, která se stále nepřeváděla správně. Nakonec jsme potřebovali data UTC v naší databázi SQL Server, aby se zarovnáli s algoritmem v programu třetí strany, který používal Java API pro převod z UTC na místní čas. .

Líbí se mi příklad CLRve výše uvedené odpovědi Kevina Feasela pomocí příkladu Harsh Chawly a rád bych to také porovnal s řešením, které používá Javu, protože naše frontend používá Java provede konverzi UTC na místní čas.

Wikipedia zmiňuje 8 různých ústavních změn, které zahrnují úpravy časového pásma před rokem 1987, a mnoho z nich je velmi lokalizováno do různých států, takže existuje šance, že je CLR a Java mohou interpretovat odlišně. Používá váš front-end kód aplikace dotnet nebo Java, nebo jsou data před rokem 1987 pro vás problém?

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

Můžete to snadno provést pomocí uložené procedury 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);
}

Dostupné časové zóny můžete uložit do tabulky:

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

A tato uložená procedura naplní tabulku možnými časovými pásmy na vašem serveru.

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

Zde je odpověď napsaná pro konkrétní britskou aplikaci a je založena čistě na SELECT.

  1. Žádné časové posuny (např. Velká Británie)
  2. Napsáno pro letní čas počínaje poslední nedělí v březnu a končící poslední říjnovou neděli (britská pravidla)
  3. Neplatí mezi půlnocí a 1:00 v den, kdy začíná letní čas. To lze opravit, ale aplikace, pro kterou byla napsána, to nevyžaduje.

    -- 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 verze 2016 vyřeší tento problém jednou provždy . U starších verzí je řešení CLR pravděpodobně nejjednodušší. Nebo pro konkrétní pravidlo DST (jako například USA) může být funkce T-SQL relativně jednoduchá.

Domnívám se však, že by mohlo být možné použít obecné řešení T-SQL. Tak dlouho jak xp_regread funguje, zkuste to:

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

A (komplexní) funkce T-SQL by mohla použít tato data k určení přesného offsetu pro všechna data během aktuálního pravidla DST.

2
Michel de Ruiter

Na základě Colinp_1 post Vytvořil jsem řešení pro převod datetime na datetimeoffset, který bere v úvahu DST a TZ. Snad to pomůže!

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