0

I would like to contribute my codes related to the date conversion between local time and UTC time for both current and historical dates, including day light savings time consideration. I reviewed some recent postings but could not find the solution I am looking for so I summarize the solutions I found to develop my own solution. I am running my codes in SQL Server 2008R2. Test cases are provided as comments before each function. Feel free to check out the Credits section for related postings, try out my codes, and let me know if I miss anything. Thanks.

-- Test 1: select dbo.fn_isDayLightSavings(CAST('2014-01-01' AS DATE)), expect 0
-- Test 2: select dbo.fn_isDayLightSavings(CAST('2014-04-14' AS DATE)), expect 1

IF OBJECT_ID(N'fn_isDayLightSavings') IS NOT NULL AND OBJECTPROPERTY(OBJECT_ID(N'fn_isDayLightSavings'),'IsScalarFunction') = 1
   DROP FUNCTION fn_isDayLightSavings;
GO
CREATE FUNCTION fn_isDayLightSavings (@dt AS DATETIME)
RETURNS TINYINT
AS
BEGIN
    DECLARE @rtn TINYINT, @year INT, @dtsStartMonth DATETIME, @dtsEndMonth DATETIME, @dstStart DATETIME, @dstEnd DATETIME;

    SET @year = DATEPART(YEAR, @dt);
    -- In year 2007, US day light savings period changes from Apr-Oct to Mar-Nov.
    if @year < 2007
    BEGIN
       -- Last Sunday of April at 2 AM
       SET @dtsStartMonth = DATEADD(MONTH, 4, DATEADD(YEAR, @year - 1900, 0));
       SET @dstStart = DATEADD(HOUR, 2, DATEADD(day, -(DATEPART(dw, @dtsStartMonth) - 1), @dtsStartMonth));
       -- Last Sunday of October at 2 AM
       SET @dtsEndMonth = DATEADD(MONTH, 10, DATEADD(YEAR, @year - 1900, 0));
       SET @dstEnd = DATEADD(HOUR, 2, DATEADD(day, -(DATEPART(dw, @dtsEndMonth) - 1), @dtsEndMonth));
    END
    else
    BEGIN
       -- 2nd Sunday of March at 2 AM
       SET @dtsStartMonth = DATEADD(MONTH, 2, DATEADD(YEAR, @year - 1900, 0));
       SET @dstStart = DATEADD(HOUR, 2, DATEADD(day, ((15 - DATEPART(dw, @dtsStartMonth)) % 7) + 7, @dtsStartMonth));
       -- 1st Sunday of November at 2 AM
       SET @dtsEndMonth = DATEADD(MONTH, 10, DATEADD(YEAR, @year - 1900, 0));
       SET @dstEnd = DATEADD(HOUR, 2, DATEADD(day, ((8 - DATEPART(dw, @dtsEndMonth)) % 7) + 7, @dtsEndMonth));
    END

    if @dt BETWEEN @dstStart AND @dstEnd SET @rtn=1 ELSE SET @rtn=0;
    RETURN @rtn;
END
GO

-- Test 1: select dbo.fn_DateTime2UTC(CAST('2014-01-01 01:00:00' AS DATETIME), 0), expect '2014-01-01 07:00:00.000'
-- Test 2: select dbo.fn_DateTime2UTC(CAST('2014-01-01 01:00:00' AS DATETIME), 1), expect '2013-01-01 07:00:00.000'
-- Test 3: select dbo.fn_DateTime2UTC(CAST('2014-05-01 01:00:00' AS DATETIME), 0), expect '2014-05-01 06:00:00.000'
-- Test 4: select dbo.fn_DateTime2UTC(CAST('2014-05-01 01:00:00' AS DATETIME), 1), expect '2014-05-01 07:00:00.000'
IF OBJECT_ID(N'fn_DateTime2UTC') IS NOT NULL AND OBJECTPROPERTY(OBJECT_ID(N'fn_DateTime2UTC'),'IsScalarFunction') = 1
   DROP FUNCTION fn_DateTime2UTC;
GO
CREATE FUNCTION fn_DateTime2UTC (@dt AS DATETIME, @ignoreDST AS TINYINT = 0)
-- do CAST(? AS DATETIMEOFFSET), if need datetimeoffset type
RETURNS DATETIME
AS
BEGIN
    DECLARE @tzOffset INT, @utcDt DATETIME;

    -- Get current time zone offset in minutes
    SET @tzOffset = DATEPART(TZoffset, SYSDATETIMEOFFSET()) +
        (CASE WHEN dbo.fn_isDayLightSavings(@dt)=1 THEN 60 ELSE 0 END);
    if dbo.fn_isDayLightSavings(@dt)=0
       set @utcDt = DATEADD(MINUTE, -@tzOffset, @dt);
    else if @ignoreDST=0
       set @utcDt = DATEADD(MINUTE, -@tzOffset, @dt);
    else
       set @utcDt = DATEADD(MINUTE, -@tzOffset+60, @dt);

    return @utcDt;
END
GO

-- Test 1: select dbo.fn_UTC2DateTime(CAST('2014-01-01 07:00:00.000' AS DATETIME), 0), expect '2014-01-01 01:00:00'
-- Test 2: select dbo.fn_UTC2DateTime(CAST('2013-01-01 07:00:00.000' AS DATETIME), 1), expect '2014-01-01 01:00:00'
-- Test 3: select dbo.fn_UTC2DateTime(CAST('2014-05-01 06:00:00.000' AS DATETIME), 0), expect '2014-05-01 01:00:00'
-- Test 4: select dbo.fn_UTC2DateTime(CAST('2014-05-01 07:00:00.000' AS DATETIME), 1), expect '2014-05-01 01:00:00'
IF OBJECT_ID(N'fn_UTC2DateTime') IS NOT NULL AND OBJECTPROPERTY(OBJECT_ID(N'fn_UTC2DateTime'),'IsScalarFunction') = 1
   DROP FUNCTION fn_UTC2DateTime;
GO
CREATE FUNCTION fn_UTC2DateTime (@utcDt AS DATETIME, @ignoreDST AS TINYINT = 0)
RETURNS DATETIME
AS
BEGIN
    DECLARE @tzOffset INT, @dt DATETIME;

    -- Get current time zone offset in minutes
    SET @tzOffset = DATEPART(TZoffset, SYSDATETIMEOFFSET()) +
        (CASE WHEN dbo.fn_isDayLightSavings(@utcDt)=1 THEN 60 ELSE 0 END);
    if dbo.fn_isDayLightSavings(@utcDt)=0
       set @dt = DATEADD(MINUTE, @tzOffset, @utcDt);
    else if @ignoreDST=0
       set @dt = DATEADD(MINUTE, @tzOffset, @utcDt);
    else
       set @dt = DATEADD(MINUTE, @tzOffset-60, @utcDt);

    return @dt;
END
GO

-- Test 1: select dbo.fn_UTC2DateTimeOffset(CAST('2014-01-01 07:00:00.000' AS DATETIME), 0), expect '2014-01-01 01:00:00.0000000 -06:00'
-- Test 2: select dbo.fn_UTC2DateTimeOffset(CAST('2013-01-01 07:00:00.000' AS DATETIME), 1), expect '2014-01-01 01:00:00.0000000 -06:00'
-- Test 3: select dbo.fn_UTC2DateTimeOffset(CAST('2014-05-01 06:00:00.000' AS DATETIME), 0), expect '2014-05-01 01:00:00.0000000 -05:00'
-- Test 4: select dbo.fn_UTC2DateTimeOffset(CAST('2014-05-01 07:00:00.000' AS DATETIME), 1), expect '2014-05-01 00:00:00.0000000 -06:00'
IF OBJECT_ID(N'fn_UTC2DateTimeOffset') IS NOT NULL AND OBJECTPROPERTY(OBJECT_ID(N'fn_UTC2DateTimeOffset'),'IsScalarFunction') = 1
   DROP FUNCTION fn_UTC2DateTimeOffset;
GO
CREATE FUNCTION fn_UTC2DateTimeOffset (@utcDt AS DATETIME, @ignoreDST TINYINT = 0)
RETURNS DATETIMEOFFSET
AS
BEGIN
    DECLARE @tzOffset INT, @dt DATETIMEOFFSET;

    -- Get current time zone offset in minutes
    SET @tzOffset = DATEPART(TZoffset, SYSDATETIMEOFFSET()) +
        (CASE WHEN dbo.fn_isDayLightSavings(@utcDt)=1 THEN 60 ELSE 0 END);
    if dbo.fn_isDayLightSavings(@utcDt)=0
       set @dt = SWITCHOFFSET(CAST(@utcDt AS DATETIMEOFFSET), @tzOffset);
    else if @ignoreDST=0
       set @dt = SWITCHOFFSET(CAST(@utcDt AS DATETIMEOFFSET), @tzOffset);
    else
       set @dt = SWITCHOFFSET(CAST(@utcDt AS DATETIMEOFFSET), @tzOffset-60);
    return @dt;
END
GO

-- Credits:
--    Determination of day light savings start and end time, Jamie F., 11/1/2013
--       (http://stackoverflow.com/questions/19732896/how-to-create-daylight-savings-time-start-and-end-function-in-sql-server)
--    Day light savings time calendar, USNO
--       (http://aa.usno.navy.mil/faq/docs/daylight_time.php)
--    Time zone offset, Microsoft
--       (http://msdn.microsoft.com/en-us/library/ms174420.aspx)
--    Local system date time with time zone, Robert Cantor, 12/7/2013
--       (http://stackoverflow.com/questions/1205142/tsql-how-to-convert-local-time-to-utc-sqlserver2008)
--    Convert date with time zone and day light savings, Eric Z Beard, 8/24/2008
--       (http://stackoverflow.com/questions/24797/effectively-converting-dates-between-utc-and-local-ie-pst-time-in-sql-2005#25073)

1 Answers1

0

Kudos to you for assembling that knowledge into one place. Assuming it is accurate, it's easy to pick up and use immediately.

However, the trouble is that without an extensive set of unit tests you can't be sure of accuracy.

Might I suggest the best option for date/time conversions would be to use an existing highly tested open source library such as http://nodatime.org/. Date/time conversions have so many intracacies and details that could not possibly be contained in a few dozen lines of SQL code.

nodatime is a .NET library that should easily be accessible via the SQL Server CLR user-defined functions feature.

agentnega
  • 3,478
  • 1
  • 25
  • 31
  • 1
    Nice thought, but neither Noda Time or TimeZoneInfo are currently accessible via sqlclr without running in "UNSAFE" mode. I am working on contributions to Noda Time for a future release to support SQLCLR as a "SAFE" mode assembly. – Matt Johnson-Pint Feb 17 '14 at 02:55