A fractional DATEDIFF for SQLServer? - sql-server

Found out something recently (and sure, my fault for not reading the docs) that reasonably horrified me about SQLServer's DATEDIFF function; it counts the number of interval boundaries between the two specified dates.
This means I can ask it for the number of days between 01 Jan 23:57 and 01 Jan 23:59 and it will return 0, but if I ask for the days between 01 Jan 23:59 and 02 Jan 00:01 it will tell me there is 1 day between them. The timespan is the same: 2 minutes, but suddenly one is a difference of 0 days and one is a difference of 1 day
Coming from an Oracle and .Net background I can see I've made a gross error in assuming that DATEDIFF in TSQL worked equivalently (i.e. prepare a timespan and then round it to the specified interval), but what is the alternative?
If I want to find out exactly, with decimal places, how many years there are between 2 dates, how do I do it in SQLServer? I don't want a result from 01 Jan to 31 Dec returning 0 years, but 31 dec 2000 to 01 jan 2002 returning 2 years, because these are gross errors and miles away from the 0.997 and 1.005 (not exact calcs) they should more likely be..
The answer clearly isn't to DATEDIFF the days and divide by 365.0, not only because datediff is routinely "wrong" even for DAYS (as per my 2 minute example) but also because there aren't always 365 days in a year. Same for months -> they aren't always a specified number of intervals long, so it doesn't make sense to take days and divide by 31 (or 30, 29, or 28).. For the same reasons I cannot do simple (endDateTime - startDateTime)/x math

If you want accuracy, you can go for minutes when doing the datediff and multiply the result accordingly. You can tweak the #dateFrom and #dateTo to test the outputs with the below code:
DECLARE #dateFrom DATETIME, #dateTo DATETIME
SET #dateFrom = '2017-07-01 23:59'
SET #dateTo = '2017-07-02 00:01'
SELECT DATEDIFF(MINUTE, #dateFrom, #dateTo) minsDiff
SELECT DATEDIFF(MINUTE, #dateFrom, #dateTo) / 60.0 hoursDiff
SELECT DATEDIFF(MINUTE, #dateFrom, #dateTo) / 60.0 / 24.0 daysDiff
SELECT DATEDIFF(MINUTE, #dateFrom, #dateTo) / 60.0 / 24.0 / 365.25 yearsDiff
SELECT DATEDIFF(MINUTE, #dateFrom, #dateTo) / 60.0 / 24.0 / 365.25 * 12 monthsDiff
It all depends on what you want to report and what your business logic is. You can do additional queries like so:
-- to track a change in days - take off the time portion:
SELECT DATEDIFF(DAY, CAST(#dateFrom AS DATE), CAST(#dateTo AS DATE)) daysDiff
-- to track a change in years - you use the year funtion:
SELECT YEAR(#dateTo) - YEAR(#dateFrom) yearsDiff

UPDATE: this code does not include DST.
You can also build your own code, which will be calculating difference between dates and put it into function.
DECLARE #dateFrom DATETIME, #dateTo DATETIME
SET #dateFrom = '2000-12-31 23:59'
SET #dateTo = '2002-01-02 00:01'
SELECT DATEDIFF(YEAR, DATEADD(year,DATEDIFF(year,0,DATEADD(year,1,#dateFrom)),0), DATEADD(year,DATEDIFF(year,0,#dateTo),0))
+ CASE WHEN YEAR(#dateFrom) < YEAR(#dateTo) THEN (DATEDIFF(MINUTE, #dateFrom, CAST(YEAR(#dateFrom) AS CHAR(4))+'-12-31 23:59') + 1) / (1.0*DATEDIFF(minute, CAST(YEAR(#dateFrom) AS CHAR(4))+'-01-01 00:00', CAST(YEAR(#dateFrom) AS CHAR(4))+'-12-31 23:59') + 1)
+ (DATEDIFF(MINUTE, CAST(YEAR(#dateTo) AS CHAR(4))+'-01-01 00:00',#dateTo ) + 1) / (1.0*DATEDIFF(minute, CAST(YEAR(#dateTo) AS CHAR(4))+'-01-01 00:00', CAST(YEAR(#dateTo) AS CHAR(4))+'-12-31 23:59') + 1)
ELSE (DATEDIFF(MINUTE,#dateFrom, #dateTo) + 1) / (1.0*DATEDIFF(minute, CAST(YEAR(#dateTo) AS CHAR(4))+'-01-01 00:00', CAST(YEAR(#dateTo) AS CHAR(4))+'-12-31 23:59') + 1) END
--Result: 1.002745428591627
This code needs comments, so:
+1 is needed to obtain exact number of minutes between dates.
This is used for calculating how many minutes year has.
(1.0*DATEDIFF(minute, CAST(YEAR(#dateTo) AS CHAR(4))+'-01-01 00:00', CAST(YEAR(#dateTo) AS CHAR(4))+'-12-31 23:59') + 1)
This give us beginning of the year.
DATEADD(year,DATEDIFF(year,0,#dateTo),0)
This piece of code is calculating full years between #dateFrom and #dateTo.
DATEDIFF(YEAR, DATEADD(year,DATEDIFF(year,0,DATEADD(year,1,#dateFrom)),0), DATEADD(year,DATEDIFF(year,0,#dateTo),0))
This is calculating "partial" years. We are calculating how many minutes left to the end of year.
(DATEDIFF(MINUTE, #dateFrom, CAST(YEAR(#dateFrom) AS CHAR(4))+'-12-31 23:59') + 1) / (1.0*DATEDIFF(minute, CAST(YEAR(#dateFrom) AS CHAR(4))+'-01-01 00:00', CAST(YEAR(#dateFrom) AS CHAR(4))+'-12-31 23:59') + 1)
Similary to what we have above but for #dateTo.
(DATEDIFF(MINUTE, CAST(YEAR(#dateTo) AS CHAR(4))+'-01-01 00:00',#dateTo ) + 1) / (1.0*DATEDIFF(minute, CAST(YEAR(#dateTo) AS CHAR(4))+'-01-01 00:00', CAST(YEAR(#dateTo) AS CHAR(4))+'-12-31 23:59') + 1)
Here, we are calculating years when #dateFrom and #dateTo have the same year part.
(DATEDIFF(MINUTE,#dateFrom, #dateTo) + 1) / (1.0*DATEDIFF(minute, CAST(YEAR(#dateTo) AS CHAR(4))+'-01-01 00:00', CAST(YEAR(#dateTo) AS CHAR(4))+'-12-31 23:59') + 1)

Related

DATEADD MINUTES Between Range MSSQL

I am having some difficulty in trying to figure out something, lets say I have a date and time;
And I want to add 180 minutes to it so;
SELECT DATEADD(MINUTE,180,'2018-05-24 15:00')
This would give me answer of "2018-05-24 18:00" but I want to do it in a range so ADD the minutes if you are between 09:00 - 17:00 so something like this;
SELECT DATEADD(MINUTES,180,'2018-05-24 15:00') WHERE '2018-05-24 15:00' BETWEEN '2018-05-24 09:00' AND '2018-05-24 17:00'
So the answer to this would be "2018-05-25 10:00"
Was hard, but this should work for all your cases. This solution works for any amount of (positive) minutes and result will always be inside the parametrized hours, adding the corresponding amount of days.
DECLARE #RangeHourStart INT = 9
DECLARE #RangeHourEnd INT = 17
DECLARE #MinutesToAdd INT = 120
DECLARE #Date DATETIME = '2018-05-24 15:00'
SELECT
FinalDate = CASE
WHEN -- When final hour exceeds the range hour
DATEPART(HOUR, #Date) * 60 +
DATEPART(MINUTE, #Date) +
#MinutesToAdd % ((#RangeHourEnd - #RangeHourStart) * 60) > #RangeHourEnd * 60
THEN
DATEADD(HOUR, -1 * (#RangeHourStart - 1),
DATEADD(DAY, 1,
DATEADD(MINUTE, #MinutesToAdd % ((#RangeHourEnd - #RangeHourStart) * 60),
DATEADD(
DAY,
#MinutesToAdd / ((#RangeHourEnd - #RangeHourStart) * 60),
#Date))))
ELSE
DATEADD(MINUTE, #MinutesToAdd % ((#RangeHourEnd - #RangeHourStart) * 60),
DATEADD(
DAY,
#MinutesToAdd / ((#RangeHourEnd - #RangeHourStart) * 60),
#Date))
END
I made it so you don't need to hard-code any value.
This doesn't look particularly pretty, however...
USE Sandbox;
GO
CREATE TABLE Times (DateNTime datetime2(0));
INSERT INTO Times
VALUES ('20180520 10:00:00'),
('20180520 15:20:00'),
('20180521 09:32:00'),
('20180521 14:17:00'),
('20180522 16:54:00'),
('20180523 12:46:00'),
('20180524 15:32:00');
GO
SELECT *
FROM Times;
GO
SELECT T.DateNTime,
CASE WHEN CONVERT(time,T.DateNTime) <= '14:00' THEN DATEADD(MINUTE, 180,T.DateNTime)
ELSE DATEADD(MINUTE, 180 - DATEDIFF(MINUTE,T.DateNTime,DATEADD(HOUR,17,DATEADD(DAY, DATEDIFF(DAY, 0, T.DateNTime),0))), DATEADD(HOUR,9,DATEADD(DAY, DATEDIFF(DAY, 0, T.DateNTime) + 1,0))) END
FROM Times T;
GO
DROP TABLE Times;
you can try this:
DECLARE #input DATETIME='2018-05-24 15:00'
DECLARE #min INT=180
SELECT CASE WHEN DATEADD(MINUTE,#min,#input)>DATEADD(HOUR, 17,DateAdd(Day, Datediff(Day,0, #input), 0))
THEN DATEADD(MINUTE,
DATEDIFF(MINUTE,
DATEADD(HOUR, 17,
DATEADD(Day,
DATEDIFF(Day,0, #input),
0)
),
DATEADD(MINUTE,#min,#input)),
DATEADD(Hour,9,
DATEADD(Day,1,
DateAdd(Day,
Datediff(Day,0, #input),
0)
)
)
)
ELSE DATEADD(MINUTE,#min,#input)
END

Convert number of hours to days and hours in SQL Server (NOT T-SQL)

I have a number of hours which I need to display in the format of days and hours.
This number is derived from a DATEDIFF instruction.
For numbers less than 24, I wish to display only hours - ie, 21 hours.
For larger numbers, I wish to display days and hours - ie, 3 days, 14 hours
I do not need to display any smaller unit than hours, and values should be rounded down to the preceding hour, so 1 hour and 59 minutes will be 1 hour.
I cannot use a stored procedure - this must run as a single select statement.
I am aware that I can calculate the value by using modulo, so assuming 71 hours:
select concat((71 - (71 % 24)) / 24, ' days, ', 71 % 24, ' hours')
This however is somewhat messy, and as the statement must be a single select, I will have to calculate the DATEDIFF 3 times as below.
SELECT CONCAT (
(DATEDIFF(HOUR, StartDate, EndDate) -
(DATEDIFF(HOUR, StartDate, EndDate) % 24)) / 24,
' days, ',
DATEDIFF(HOUR, StartDate, EndDate) % 24,
' hours')
FROM RecordsTable
Is it possible to either format a number of hours as days and hours directly using an inbuilt SQL command, or failing that, select (datediff(hour, StartDate, EndDate) into a variable which I can reuse in the single select?
EDIT - As suggested, the solution was to use a CTE as follows:
WITH totalhours (htotal) AS
(
SELECT
DATEDIFF(HOUR, StartDate, EndDate) AS htotal
FROM
RecordsTable
)
SELECT
CONCAT ((htotal - (htotal % 24)) / 24,
' days, ',
htotal % 24,
' hours')
FROM
RecordsTable;
Use a CTE to generate your total once, and reference that total in your select against the CTE. Or use a subquery to generate the total once and then select from the subquery to get the desired results.
The fundamental issue is you need to materialize the total once to be able to reference it; forcing the engine to materialize a value is generally done via a CTE or subquery.
You can do a lot with datetime objects and format strings or datepart. For example,
declare #n int = 105;
select format(dateadd(day, -1, dateadd(hour, #n, '1753-1-1')), 'd h');
-- 4 9
Taking the minimum datetime value (1753-01-01), adding the requisite number of hours, subtracting one day (because on the first day you want days = 0), and then formatting.
You could improve the formatting like this:
select format(dateadd(day, -1, dateadd(hour, #n, '1753-1-1')), 'd \da\y(\s), h \hour(\s)');
-- 4 day(s), 9 hour(s)
Of course this will only work up to 31 days, because then you'll be out of the month of January in 1753 and into February. If that's the case, revert to datepart. This is uglier, but will work for larger values
select
datepart(day, (dateadd(day, -1, dateadd(hour, #n, '1753-1-1')))),
datepart(hour, (dateadd(day, -1, dateadd(hour, #n, '1753-1-1'))));

sql not in between two weekdays and times

I have a query that monitors connection process. Now I'm stuck and need to set a proper monitoring for weekday and time range.
The process starts on Sunday 22:00, and goes down for 5 min. at 21:55 - every day to Friday. (not goes up from Friday 21:55 till 22:00 on Sunday)
Below is the SQL Query I tried:
IF CASE
WHEN (100 * DATEPART(hh, GETDATE()))
+ DATEPART(MINUTE, GETDATE())
BETWEEN 2155 AND 2200 -- Monitoring for whole day, wen connection is up
AND DATEPART(dw,GETDATE()), (100 * DATEPART(hh, GETDATE()))
+ DATEPART(MINUTE, GETDATE())
NOT BETWEEN (5, 2155) AND (0, 2200) --except trough Friday night to Sunday (weekdays and time).
THEN 1 ELSE 0 END = 0
You needed to add some SELECT statements into the parts of the case where you were getting the values to range between. Try this:
SELECT
CASE
WHEN(100 * DATEPART(hh, GETDATE())) + DATEPART(MINUTE, GETDATE()) -- = 731
BETWEEN 2155 AND 2200 -- Monitoring for whole day, wen connection is up
AND (
(
SELECT
DATEPART(dw, GETDATE())
) NOT BETWEEN(5) AND(0)
AND
(
SELECT
(100 * DATEPART(hh, GETDATE())) + DATEPART(MINUTE, GETDATE())
) NOT BETWEEN(2155) AND(2200)
)
THEN 1
ELSE 0
END;
The first calculation equals 731 so looking at the query I would except it to return '0' which it does

how to convert DateDiff back to datetime?

datediff(second,#date1, #date2)
i.e. exact difference between 2004-09-01 09:56:11.000 and 2005-02-02 08:54:02.000...output should be 5 months, x days, y hours,z minutes, m seconds.
You need that as hours and minutes? You can use dateadd to 0 date:
dateadd(second, amount, 0)
And then convert this into suitable format using convert and the formatting options like 108.
convert(varchar, dateadd(second, amount, 0), 108)
This is way more difficult than I originally thought, Calculating months and days is complicated. Here is an attempt. You should test it carefully, I imagine leapyear could also cause problems for the year calculation, so I removed it from the answer since it is not part of your question:
DECLARE #date1 datetime = '2004-09-01 09:56:11.000',
#date2 datetime = '2005-02-02 08:54:02.000'
SELECT
#date2 - #date1 difference,
datediff(month, #date1, #date2) +
CASE WHEN dateadd(month, datediff(month, #date1, #date2), #date1)>#date2
THEN -1
ELSE 0 END month,
day(#date2 - dateadd(month, datediff(month, #date1, #date2)
+ CASE WHEN dateadd(month, datediff(month, #date1, #date2), #date1)>#date2
THEN -1 ELSE 0
END, #date1)) - 1 day,
datepart(hour,#date2 - #date1) hour,
datepart(minute,#date2 - #date1) minute,
datepart(second,#date2 - #date1) second
Result now:
difference month day hour minute second
1900-06-03 22:57:51.000 5 0 22 57 51
Note: This answer will not give you exactly the same result, but it will be more accurate with seconds being rounded down.
More edit:
If you can accept not having the months as described in your comment and can accept this format x days hh:MM:ss. You can use this syntax:
SELECT
CAST((DateDiff(SECOND, #date1, #date2)) / 86400 AS varchar(7)) + ' days '
+ CAST(cast(#date2 - #date1 as time(0)) as char(8))
datediff has 3 parameters, not 2: datediff(daypart, startdate, enddate). If you need difference in seconds, use datediff(second, #date1, #date2).
To convert it back to datetime, use dateadd(datepart, number, startdate). E.g. dateadd(second, #diffResult, '1900-01-01'). For start date choose one that fits your needs.

Effectively Converting dates between UTC and Local (ie. PST) time in SQL 2005

What is the best way to convert a UTC datetime into local datetime. It isn't as simple as a getutcdate() and getdate() difference because the difference changes depending on what the date is.
CLR integration isn't an option for me either.
The solution that I had come up with for this problem a few months back was to have a daylight savings time table that stored the beginning and ending daylight savings days for the next 100 or so years, this solution seemed inelegant but conversions were quick (simple table lookup)
Create two tables and then join to them to convert stored GMT dates to local time:
TimeZones e.g.
--------- ----
TimeZoneId 19
Name Eastern (GMT -5)
Offset -5
Create the daylight savings table and populate it with as much information as you can (local laws change all the time so there's no way to predict what the data will look like years in the future)
DaylightSavings
---------------
TimeZoneId 19
BeginDst 3/9/2008 2:00 AM
EndDst 11/2/2008 2:00 AM
Join them like this:
inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId
left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone
and x.TheDateToConvert between ds.BeginDst and ds.EndDst
Convert dates like this:
dateadd(hh, tz.Offset +
case when ds.LocalTimeZone is not null
then 1 else 0 end, TheDateToConvert)
If you're in the US and only interested in going from UTC/GMT to a fixed time zone (such as EDT) this code should suffice. I whipped it up today and believe it's correct but use at your own risk.
Adds a computed column to a table 'myTable' assuming your dates are on the 'date' column. Hope someone else finds this useful.
ALTER TABLE myTable ADD date_edt AS
dateadd(hh,
-- The schedule through 2006 in the United States was that DST began on the first Sunday in April
-- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006).
-- The time is adjusted at 02:00 local time.
CASE WHEN YEAR(date) <= 2006 THEN
CASE WHEN
date >= '4/' + CAST(abs(8-DATEPART(dw,'4/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
AND
date < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date) as varchar)) as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
THEN -4 ELSE -5 END
ELSE
-- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007.
-- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on
-- the first Sunday of November, one week later than in years past. This change resulted in a new DST period
-- that is four weeks (five in years when March has five Sundays) longer than in previous years.[35] In 2008
-- daylight saving time ended at 02:00 on Sunday, November 2, and in 2009 it began at 02:00 on Sunday, March 8.[36]
CASE WHEN
date >= '3/' + CAST(abs(8-DATEPART(dw,'3/1/' + CAST(YEAR(date) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
AND
date <
'11/' + CAST(abs(8-DATEPART(dw,'11/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00'
THEN -4 ELSE -5 END
END
,date)
FOR READ-ONLY Use this(inspired by Bob Albright's incorrect solution ):
SELECT
date1,
dateadd(hh,
-- The schedule through 2006 in the United States was that DST began on the first Sunday in April
-- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006).
-- The time is adjusted at 02:00 local time (which, for edt, is 07:00 UTC at the start, and 06:00 GMT at the end).
CASE WHEN YEAR(date1) <= 2006 THEN
CASE WHEN
date1 >= '4/' + CAST((8-DATEPART(dw,'4/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 7:00'
AND
date1 < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date1) as varchar)) as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 6:00'
THEN -4 ELSE -5 END
ELSE
-- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007.
-- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on
-- the first Sunday of November, one week later than in years past. This change resulted in a new DST period
-- that is four weeks (five in years when March has five Sundays) longer than in previous years. In 2008
-- daylight saving time ended at 02:00 edt (06:00 UTC) on Sunday, November 2, and in 2009 it began at 02:00 edt (07:00 UTC) on Sunday, March 8
CASE WHEN
date1 >= '3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(date1) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 7:00'
AND
date1 < '11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 6:00'
THEN -4 ELSE -5 END
END
, date1) as date1Edt
from MyTbl
I posted this answer after I tried to edit Bob Albright's wrong answer. I corrected the times and removed superfluous abs(), but my edits were rejected multiple times. I tried explaining, but was dismissed as a noob. His is a GREAT approach to the problem! It got me started in the right direction. I hate to create this separate answer when his just needs a minor tweak, but I tried ¯\_(ツ)_/¯
A much simpler and generic solution that considers daylight savings. Given an UTC date in "YourDateHere":
--Use Minutes ("MI") here instead of hours because sometimes
-- the UTC offset may be half an hour (e.g. 9.5 hours).
SELECT DATEADD(MI,
DATEDIFF(MI, SYSUTCDATETIME(),SYSDATETIME()),
YourUtcDateHere)[LocalDateTime]
If either of these issues affects you, you should never store local times in the database:
With DST is that there is an "hour of uncertainty" around the falling back period where a local time cannot be unambiguously converted. If exact dates & times are required, then store in UTC.
If you want to show users the date & time in their own timezone, rather than the timezone in which the action took place, store in UTC.
In Eric Z Beard's answer, the following SQL
inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId
left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone
and x.TheDateToConvert between ds.BeginDst and ds.EndDst
might more accurately be:
inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId
left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone
and x.TheDateToConvert >= ds.BeginDst and x.TheDateToConvert < ds.EndDst
(above code not tested)
The reason for this is that the sql "between" statement is inclusive. On the back-end of DST, this would result in a 2AM time NOT being converted to 1AM. Of course the likelihood of the time being 2AM precisely is small, but it can happen, and it would result in an invalid conversion.
Maintain a TimeZone table, or shell out with an extended stored proc (xp_cmdshell or a COM component, or your own) and ask the OS to do it. If you go the xp route, you'd probably want to cache the offset for a day.
I like the answer #Eric Z Beard provided.
However, to avoid performing a join everytime, what about this?
TimeZoneOffsets
---------------
TimeZoneId 19
Begin 1/4/2008 2:00 AM
End 1/9/2008 2:00 AM
Offset -5
TimeZoneId 19
Begin 1/9/2008 2:00 AM
End 1/4/2009 2:00 AM
Offset -6
TimeZoneId 20 --Hong Kong for example - no DST
Begin 1/1/1900
End 31/12/9999
Offset +8
Then
Declare #offset INT = (Select IsNull(tz.Offset,0) from YourTable ds
join TimeZoneOffsets tz on tz.TimeZoneId=ds.LocalTimeZoneId
and x.TheDateToConvert >= ds.Begin and x.TheDateToConvert < ds.End)
finally becoming
dateadd(hh, #offset, TheDateToConvert)
I've read through a lot of StackOverflow posts in regards to this issue and found many methods. Some "sort of" ok. I also found this MS reference (https://msdn.microsoft.com/en-us/library/mt612795.aspx) which I tried to utilize in my script. I have managed to achieve the required result BUT I am not sure if this will run on 2005 version. Either way, I hope this helps.
Fnc to return PST from the system UTC default
CREATE FUNCTION dbo.GetPst()
RETURNS DATETIME
AS
BEGIN
RETURN SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time'
END
SELECT dbo.GetPst()
Fnc to return PST from the provided timestamp
CREATE FUNCTION dbo.ConvertUtcToPst(#utcTime DATETIME)
RETURNS DATETIME
AS
BEGIN
RETURN DATEADD(HOUR, 0 - DATEDIFF(HOUR, CAST(SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time' AS DATETIME), SYSDATETIME()), #utcTime)
END
SELECT dbo.ConvertUtcToPst('2016-04-25 22:50:01.900')
I am using this because all of my dates are from now forward.
DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, GETDATE())
For historical dates (or to handle future changes in DST, I'm guessing Bob Albright's solution would be the way to go.
The modification I make to my code is to use the target column:
DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, [MySourceColumn])
So far, this seems to work, but I'm happy to receive feedback.
Here is the code I use to make my timezone table. It's a bit naive, but is usually good enough.
Assumptions:
It assumes US only rules (DST is 2AM on some pre-defined Sunday,
etc).
It assumes you don't have dates prior to 1970
It assumes you know the local timezone offsets (i.e.: EST=-05:00, EDT=-04:00, etc.)
Here's the SQL:
-- make a table (#dst) of years 1970-2101. Note that DST could change in the future and
-- everything was all custom and jacked before 1970 in the US.
declare #first_year varchar(4) = '1970'
declare #last_year varchar(4) = '2101'
-- make a table of all the years desired
if object_id('tempdb..#years') is not null drop table #years
;with cte as (
select cast(#first_year as int) as int_year
,#first_year as str_year
,cast(#first_year + '-01-01' as datetime) as start_of_year
union all
select int_year + 1
,cast(int_year + 1 as varchar(4))
,dateadd(year, 1, start_of_year)
from cte
where int_year + 1 <= #last_year
)
select *
into #years
from cte
option (maxrecursion 500);
-- make a staging table of all the important DST dates each year
if object_id('tempdb..#dst_stage') is not null drop table #dst_stage
select dst_date
,time_period
,int_year
,row_number() over (order by dst_date) as ordinal
into #dst_stage
from (
-- start of year
select y.start_of_year as dst_date
,'start of year' as time_period
,int_year
from #years y
union all
select dateadd(year, 1, y.start_of_year)
,'start of year' as time_period
,int_year
from #years y
where y.str_year = #last_year
-- start of dst
union all
select
case
when y.int_year >= 2007 then
-- second sunday in march
dateadd(day, ((7 - datepart(weekday, y.str_year + '-03-08')) + 1) % 7, y.str_year + '-03-08')
when y.int_year between 1987 and 2006 then
-- first sunday in april
dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-01')) + 1) % 7, y.str_year + '-04-01')
when y.int_year = 1974 then
-- special case
cast('1974-01-06' as datetime)
when y.int_year = 1975 then
-- special case
cast('1975-02-23' as datetime)
else
-- last sunday in april
dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-24')) + 1) % 7, y.str_year + '-04-24')
end
,'start of dst' as time_period
,int_year
from #years y
-- end of dst
union all
select
case
when y.int_year >= 2007 then
-- first sunday in november
dateadd(day, ((7 - datepart(weekday, y.str_year + '-11-01')) + 1) % 7, y.str_year + '-11-01')
else
-- last sunday in october
dateadd(day, ((7 - datepart(weekday, y.str_year + '-10-25')) + 1) % 7, y.str_year + '-10-25')
end
,'end of dst' as time_period
,int_year
from #years y
) y
order by 1
-- assemble a final table
if object_id('tempdb..#dst') is not null drop table #dst
select a.dst_date +
case
when a.time_period = 'start of dst' then ' 03:00'
when a.time_period = 'end of dst' then ' 02:00'
else ' 00:00'
end as start_date
,b.dst_date +
case
when b.time_period = 'start of dst' then ' 02:00'
when b.time_period = 'end of dst' then ' 01:00'
else ' 00:00'
end as end_date
,cast(case when a.time_period = 'start of dst' then 1 else 0 end as bit) as is_dst
,cast(0 as bit) as is_ambiguous
,cast(0 as bit) as is_invalid
into #dst
from #dst_stage a
join #dst_stage b on a.ordinal + 1 = b.ordinal
union all
select a.dst_date + ' 02:00' as start_date
,a.dst_date + ' 03:00' as end_date
,cast(1 as bit) as is_dst
,cast(0 as bit) as is_ambiguous
,cast(1 as bit) as is_invalid
from #dst_stage a
where a.time_period = 'start of dst'
union all
select a.dst_date + ' 01:00' as start_date
,a.dst_date + ' 02:00' as end_date
,cast(0 as bit) as is_dst
,cast(1 as bit) as is_ambiguous
,cast(0 as bit) as is_invalid
from #dst_stage a
where a.time_period = 'end of dst'
order by 1
-------------------------------------------------------------------------------
-- Test Eastern
select
the_date as eastern_local
,todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end) as eastern_local_tz
,switchoffset(todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end), '+00:00') as utc_tz
--,b.*
from (
select cast('2015-03-08' as datetime) as the_date
union all select cast('2015-03-08 02:30' as datetime) as the_date
union all select cast('2015-03-08 13:00' as datetime) as the_date
union all select cast('2015-11-01 01:30' as datetime) as the_date
union all select cast('2015-11-01 03:00' as datetime) as the_date
) a left join
#dst b on b.start_date <= a.the_date and a.the_date < b.end_date
--Adapted Bob Albright and WillDeStijl suggestions for SQL server 2014
--
--In this instance I had no dates prior to 2006, therefore I simplified the case example
--I had to add the variables for the assignment to allow trimming the timestamp from my resultset
DECLARE #MARCH_DST as DATETIME
SET #MARCH_DST='3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(getdate()) as varchar) + ' 7:00'
DECLARE #NOV_DST as DATETIME
SET #NOV_DST='11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(getdate()) as varchar) + ' 6:00'
select cast(dateadd(HOUR,
-- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007.
-- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on
-- the first Sunday of November, one week later than in years past. This change resulted in a new DST period
-- that is four weeks (five in years when March has five Sundays) longer than in previous years. In 2008
-- daylight saving time ended at 02:00 edt (06:00 UTC) on Sunday, November 2, and in 2009 it began at 02:00 edt (07:00 UTC) on Sunday, March 8
CASE WHEN
date1 >=#MARCH_DST
AND
date1< #NOV_DST
THEN -4 ELSE -5 END
, date1) as DATE) as date1_edited
I found Simple Way to convert any date to any timezone.
Currently i have changed date to India Standard Time
DECLARE #SqlServerTimeZone VARCHAR(50)
DECLARE #LocalTimeZone VARCHAR(50)='India Standard Time'
EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE',
'SYSTEM\CurrentControlSet\Control\TimeZoneInformation',
'TimeZoneKeyName',#SqlServerTimeZone OUT
DECLARE #DateToConvert datetime= GetDate()
SELECT LocalDate = #DateToConvert AT TIME ZONE #SqlServerTimeZone AT TIME ZONE #LocalTimeZone

Resources