Нужна функция SQL Server для преобразования локальной даты и времени в UTC, которая поддерживает DST

Мы переносим приложение в Azure, но в настоящее время все наши даты хранятся в восточном стандартном времени. Поскольку SQL Azure находится в UTC, и многие наши даты генерируются с помощью вызова getdate (), у нас будут проблемы, если мы оставим все как есть. Кажется, что вариант, который обеспечит наименьшую долговременную головную боль, состоит в том, чтобы просто преобразовать все наши сохраненные даты в UTC и преобразовать их в местное время во внешнем интерфейсе.

К сожалению, похоже, что нет встроенного способа преобразования даты и времени SQL из одного часового пояса в другой, и многие из найденных мной решений не учитывают переход на летнее время. Решения, которые я нашел, которые рассматривают DST, включают импорт календарей и таблиц часовых поясов и являются чрезвычайно сложными.

Я просто ищу что-то, что будет работать для одного преобразования, и это не должно быть красиво. Есть идеи?

ОБНОВЛЕНИЕ: я написал следующую функцию, которая, я считаю, выполнит то, что мне нужно. Кто-нибудь видит какие-либо дыры с этим? Все мои даты в настоящее время соответствуют восточному стандартному времени, но мои даты начинаются примерно в 2002 году, поэтому я должен был справиться с этим изменением закона в 2007 году.

Я думаю, я бы использовал эту функцию следующим образом:

UPDATE myTable SET myDate = dbo.fnESTtoUTC(myDate)

Вот функция:

CREATE FUNCTION [dbo].[fnESTtoUTC]
    (@pESTDate  DATETIME)
    RETURNS DATETIME
AS
BEGIN

DECLARE @TimeZoneOffset INT
DECLARE @Year INT
DECLARE @Day INT
DECLARE @DSTStart DATETIME
DECLARE @DSTEnd DATETIME

SELECT @Year = DATEPART(year, @pESTDate)

IF @Year >= 2007
BEGIN
    SELECT @Day = 8
    WHILE @DSTStart IS NULL
    BEGIN
        -- Second Sunday in March
        IF DATEPART(weekday, DATEFROMPARTS(@Year, 3, @Day)) = 1
            SELECT @DSTStart = DATETIMEFROMPARTS(@Year, 3, @Day, 2, 0, 0, 0)
        SELECT @Day = @Day + 1
    END

    SELECT @Day = 1
    WHILE @DSTEnd IS NULL
    BEGIN
        -- First Sunday in November
        IF DATEPART(weekday, DATEFROMPARTS(@Year, 11, @Day)) = 1
            SELECT @DSTEnd = DATETIMEFROMPARTS(@Year, 11, @Day, 1, 0, 0, 0)
        SELECT @Day = @Day + 1
    END
END
ELSE
BEGIN
    SELECT @Day = 1
    WHILE @DSTStart IS NULL
    BEGIN
        -- First Sunday in April
        IF DATEPART(weekday, DATEFROMPARTS(@Year, 4, @Day)) = 1
            SELECT @DSTStart = DATETIMEFROMPARTS(@Year, 4, @Day, 2, 0, 0, 0)
        SELECT @Day = @Day + 1
    END

    SELECT @Day = 31
    WHILE @DSTEnd IS NULL
    BEGIN
        -- Last Sunday in October
        IF DATEPART(weekday, DATEFROMPARTS(@Year, 10, @Day)) = 1
            SELECT @DSTEnd = DATETIMEFROMPARTS(@Year, 10, @Day, 1, 0, 0, 0)
        SELECT @Day = @Day - 1
    END
END

IF @pESTDate >= @DSTStart AND @pESTDate < @DSTEnd
BEGIN
    -- Date is in DST
    SELECT @TimeZoneOffset = 4
END
ELSE
BEGIN
    -- Not DST
    SELECT @TimeZoneOffset = 5
END

RETURN ( dateadd(hh, @TimeZoneOffset, @pESTDate) )
END

2 ответа

Решение

Есть несколько проблем:

  • Вы основываете, какое правило использовать на текущей дате, а не на дате, указанной в ваших входных данных. Вы должны изменить это наверняка.

  • Если я передам значение, например 2013-03-10 02:30, ваша функция будет считать, что это было EDT, но в действительности это время было недействительным и не должно существовать в ваших данных. Вы, вероятно, должны поднять ошибку.

  • Если я передам значение, например 2013-11-03 01:30, ваша функция будет считать, что это было EDT, но в действительности это могло быть либо в EDT, либо в EST. Вы должны были бы сохранить или смещение, или флаг dst для устранения неоднозначности. Если это не в данных, у вас нет выбора, кроме как принять одно или другое.

  • Эта функция не учитывает даты до 1987 года, когда в Соединенных Штатах также было изменено правило DST. Если у вас есть данные до этого, вы должны учитывать это.

Кроме того, это выглядит хорошо. Тем не менее, точки в комментариях верны. Это будет работать только для этого часового пояса, и у вас нет никаких гарантий, что правила для этого часового пояса не изменятся в будущем. Я рекомендую вам использовать конвертировать ваши данные, чтобы использовать UTC в будущем. Вы можете использовать эту функцию для преобразования, если хотите, или вы можете так же легко сделать это в коде уровня приложения.

Ох, и еще одна вещь. "Восточное стандартное время" или "EST" буквально означает UTC-5 без учета летнего времени вообще. Точно так же как "Восточное летнее время" или "EDT" всегда означает UTC-4. Я предполагаю, что вы хотели сказать, что ваши данные находятся в "восточном времени" в вашем вопросе, который учитывает и то, и другое.

Если вы действительно имели в виду EST, то ваша работа намного проще - просто добавьте 5 часов и сделайте это. Я говорю об этом, потому что действительно есть сценарии, в которых данные записываются без учета летнего времени. (Я полагаю, что в финансовом секторе есть несколько вариантов использования, которые так работают.)

Я только что создал собственный, который принимает переменную datetime в будущем. Он следует правилам NIST, изложенным ниже. Однако я должен был вернуть его как TEXT, а не как datetime, и мне пришлось выбрать MS SQL 2005. Не стесняйтесь настраивать.:)

Правила основаны на www.nist.gov

  • Начните второе воскресенье марта @ 02:00
  • Конец первого воскресенья ноября @ 02:00

Мой часовой пояс: Западное побережье (-8)

CREATE FUNCTION [dbo].[Local_Time]
(
    @dt     datetime    --  In UTC
)
RETURNS varchar(40)
AS
BEGIN

declare @local as datetime
declare @dw as int
declare @startDLS as datetime
declare @stopDLS as datetime
declare @tz as int
set @tz = -8        -- Time Zone - AKA sunny (and expensive) California

set @startDLS = 0  -- init
set @startDLS = cast (dateadd (yy, datepart(yy,@dt)-1900, @startDLS) as datetime)       --  Current Year
set @stopDLS = @startDLS                                                            --     Current Year
set @startDLS = cast (dateadd (mm, 2, @startDLS) as datetime)   -- Make date March 1st
set @stopDLS  = cast (dateadd (mm, 10, @stopDLS) as datetime)   -- Make date November 1st
set @dw = datepart(dw, @startDLS)

IF @dw = 1  
    BEGIN
        set @startDLS = cast (dateadd (dd, 7 , @startDLS) as datetime)
    END
ELSE 
    BEGIN
        set @startDLS = cast (dateadd (dd, 15-@dw, @startDLS) as datetime)
    END
SET @startDLS = cast (dateadd (hh, 2, @startDLS) as datetime)

IF @dw = 1 
    BEGIN
        set @stopDLS = cast (dateadd (dd, 7 , @stopDLS) as datetime)
    END
ELSE 
    BEGIN
        set @stopDLS = cast (dateadd (dd, 8-@dw, @stopDLS) as datetime)
    END
SET @stopDLS = cast (dateadd (hh, 2, @stopDLS) as datetime)

IF (@dt >= @startDLS ) and 
    (@dt < @stopDLS )
    BEGIN  --  @dt is within Daylight Savings Time Rules - Add an hour
        set @local = dateadd(hh, -7, @dt)
    END
ELSE    --  @dt is outside Daylight Savings Time Rules - No Adjustment
    BEGIN
        set @local = dateadd(hh, -8, @dt)
    END
return cast(@local as varchar(40) )

END
Другие вопросы по тегам