need to convert sql time from decimal to hours - sql

I have this code:
cast( DateDiff( MINUTE, S.PlannedStartDateTime, S.PlannedEndDateTime) as decimal (12,6)) / 60 else null end) BasicHours
which brings back the date time as decimal i.e. 31.416666, but this really needs to be 31hr 25mins
How can my code display the correct date format?

Yet another option.
Example
Declare #YourTable table (id int,StartTime datetime,EndTime datetime)
Insert Into #YourTable values
(1,'2017-01-01 20:30:15','2017-01-05 22:58:35'),
(2,'2017-01-01 09:00:00','2017-01-01 17:00:00'),
(3,'2017-01-01 09:00:00','2017-01-01 09:05:00')
Select A.ID
,Duration = concat(Seconds/3600,'hr ',Seconds%3600/60,'mins')
From #YourTable A
Cross Apply (values (DateDiff(SECOND,StartTime,EndTime))) B (Seconds)
Returns
ID Duration
1 98hr 28mins
2 8hr 0mins
3 0hr 5mins
EDIT if you don't want the CROSS APPLY
Select A.ID
,Duration = concat(DateDiff(SECOND,StartTime,EndTime)/3600,'hr ',DateDiff(SECOND,StartTime,EndTime)%3600/60,'mins')
From #YourTable A
One Should Note:, there is a limitation ... the max value of an INT (or 2,147,483,647). So provided your span does not exceed:
Years Months Days Hours Minutes Seconds
68 0 19 3 14 7

This is my version of Itzik Ben Gan's DATEDIFFPARTS function
CREATE FUNCTION dbo.tfn_DateDiffParts
/* =======================================================================================================
04/06/2017 JL, Created.
Code based off of a similar function created by Itzik Ben-Gan.
(http://sqlmag.com/sql-server/how-compute-date-and-time-difference-parts)
======================================================================================================= */
(
#dt1 AS DATETIME2(7),
#dt2 AS DATETIME2(7)
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT
DateDiffParts = CONCAT(
CAST(V.yy AS VARCHAR(10)) + L.yy,
CAST(V.mm AS VARCHAR(10)) + L.mm,
CAST(V.dd AS VARCHAR(10)) + L.dd,
CAST(V.hh AS VARCHAR(10)) + L.hh,
CAST(V.mi AS VARCHAR(10)) + L.mi,
CAST(V.ss AS VARCHAR(10)) + L.ss
)
FROM
( VALUES (
CASE WHEN #dt1 > #dt2 THEN #dt2 ELSE #dt1 END,
CASE WHEN #dt1 > #dt2 THEN #dt1 ELSE #dt2 END
)
) D (dt1, dt2)
CROSS APPLY ( VALUES (
CAST(D.dt1 AS TIME),
CAST(D.dt2 AS TIME),
DATEDIFF(yy, D.dt1, D.dt2),
DATEDIFF(mm, D.dt1, D.dt2),
DATEDIFF(dd, D.dt1, D.dt2)
)
) A1 (t1, t2, yydiff, mmdiff, dddiff)
CROSS APPLY ( VALUES (
CASE WHEN DATEADD(yy, A1.yydiff, D.dt1) > D.dt2 THEN 1 ELSE 0 END,
CASE WHEN DATEADD(mm, A1.mmdiff, D.dt1) > D.dt2 THEN 1 ELSE 0 END,
CASE WHEN DATEADD(dd, A1.dddiff, D.dt1) > D.dt2 THEN 1 ELSE 0 END
)
) A2 (subyy, submm, subdd)
CROSS APPLY ( VALUES (
CAST(86400000000000 AS BIGINT) * A2.subdd
+ (CAST(1000000000 AS BIGINT) * DATEDIFF(ss, '00:00', A1.t2) + DATEPART(ns, A1.t2))
- (CAST(1000000000 AS BIGINT) * DATEDIFF(ss, '00:00', A1.t1) + DATEPART(ns, A1.t1))
)
) A3 (nsdiff)
CROSS APPLY ( VALUES (
A1.yydiff - A2.subyy,
(A1.mmdiff - A2.submm) % 12,
DATEDIFF(DAY, DATEADD(mm, A1.mmdiff - A2.submm,D.dt1), D.dt2) - A2.subdd,
A3.nsdiff / CAST(3600000000000 AS BIGINT),
A3.nsdiff / CAST(60000000000 AS BIGINT) % 60,
CAST(A3.nsdiff / 1000000000.0 % 60.0 AS DECIMAL(9,1))
)
) V (yy, mm, dd, hh, mi, ss)
CROSS APPLY ( VALUES (
CASE
WHEN V.yy > 1 THEN ' years, '
WHEN V.yy > 0 THEN ' year, '
END,
CASE
WHEN V.mm > 1 THEN ' months, '
WHEN V.mm > 0 THEN ' month, '
WHEN V.yy > 0 THEN ' months, '
END,
CASE
WHEN V.dd > 1 THEN ' days, '
WHEN V.dd > 0 THEN ' day, '
WHEN V.yy > 0 OR V.mm > 0 THEN ' days, '
END,
CASE
WHEN V.hh > 1 THEN ' hours, '
WHEN V.hh > 0 THEN ' hour, '
WHEN V.yy > 0 OR V.mm > 0 OR V.dd > 0 THEN ' hours, '
END,
CASE
WHEN V.mi > 1 THEN ' mins, '
WHEN V.mi > 0 THEN ' min, '
WHEN V.yy > 0 OR V.mm > 0 OR V.dd > 0 OR V.hh > 0 THEN ' mins, '
END,
CASE
WHEN V.ss > 1 THEN ' secs'
WHEN V.ss > 0 THEN ' sec'
WHEN V.yy > 0 OR V.mm > 0 OR V.dd > 0 OR V.hh > 0 OR V.mi > 0 THEN ' secs'
END
)
) L (yy, mm, dd, hh, mi, ss);
GO
Sample query...
SELECT
ddp.DateDiffParts
FROM
dbo.tfn_DateDiffParts('2017-06-01 08:22:11.12345', '2017-06-02 11:30:25.32145') ddp;
And the function output...
DateDiffParts
--------------------------------------------------------------------------------------------------------
1 day, 3 hours, 8 mins, 14.2 secs

you can use below query to get your result -
select CAST (CONCAT( datediff(HH,'2017-09-05 21:55:00','2017-09-07 08:16:00') , '.' , 60- datediff(MINUTE,'2017-09-05 21:55:00','2017-09-07 08:16:00')%60) as decimal (12,6))
--OUTPUT
35.390000

Related

A way to get averages over a period of days

I have a table with five years worth of data. It has, among other values, JulianDate, RefETo, and Precipitation.
I need to get the average of RefETo over that five years and the average of the last year, plus the sum of precipitation over the last year.
At the same time the averages and sums have to be for a period of time between 7 and 28 days.
Right now, I'm doing it with a function:
FUNCTION [dbo].[CIMISAvg](#Stn INT, #Yr INT, #Period INT)
RETURNS #AvgTable TABLE (Period INT, RefETo float, RefETo1 float, Precipitation float)
AS
BEGIN
DECLARE #PeriodInc INT = 1
DECLARE #RefETo float
DECLARE #Precip float
DECLARE #RefETo1 float
DECLARE #P INT = 1
BEGIN
WHILE #PeriodInc < 366
BEGIN
IF #PeriodInc < 365
BEGIN
SET #RefETo = (SELECT AVG(RefETo) FROM Cimis
WHERE StationNo = #Stn AND RefETO >= 0 AND JulianDate BETWEEN #PeriodINC AND
PeriodINC + #Period - 1)
SET #RefETo1 = (SELECT AVG(RefETo) FROM Cimis WHERE StationNo = #Stn AND
RefETO >= 0 AND JulianDate BETWEEN #PeriodINC AND #PeriodINC + #Period - 1
AND DATEPART(Year, DateCollected) = #Yr)
SET #Precip = (SELECT SUM(Precipitation) FROM Cimis
WHERE StationNo = #Stn AND Precipitation >= 0 AND JulianDate
BETWEEN #PeriodINC AND #PeriodINC + #Period - 1
AND DATEPART(Year, DateCollected) = #Yr)
END
ELSE
BEGIN
SET #RefETo = (SELECT AVG(RefETo) FROM Cimis
WHERE StationNo = #Stn AND RefETO >= 0
AND (JulianDate > 364 OR JulianDate < #Period - 1))
SET #RefETo1 = (SELECT AVG(RefETo) FROM Cimis WHERE StationNo = #Stn
AND RefETO >= 0 AND JulianDate > 364
AND DATEPART(Year, DateCollected) = #Yr)
SET #Precip = (SELECT SUM(Precipitation) FROM Cimis WHERE StationNo = #Stn
AND Precipitation >= 0 AND JulianDate > 364
AND DATEPART(Year, DateCollected) = #Yr)
END
INSERT INTO #AvgTable(Period, RefETo, RefETo1, Precipitation)
VALUES (#P, #RefETo, #RefETo1, #Precip)
SET #PeriodInc += #Period
SET #P += 1
END
END
RETURN
END
It returns the following table if I use:
SELECT * from dbo.CimiAvg(80,2014,28)
Period RefETo RefETo1 Precipitation
1 0.0417192857142857 0.0470392857142857 0.0156
2 0.0672328571428571 0.0585214285714286 0
3 0.121372142857143 0.135967857142857 1.2755
4 0.170277519379845 0.186428571428571 0.7991
5 0.235207258064516 0.240425 0.7087
6 0.268260240963855 0.294403571428571 0.1811
7 0.293128125 0.290282142857143 0
8 0.273767123287671 0.267457142857143 0.0196
9 0.244358333333333 0.2513375 0
10 0.176087142857143 NULL NULL
11 0.10749 NULL NULL
12 0.0625579831932773 NULL NULL
13 0.0382158273381295 NULL NULL
14 0.0413401459854015 NULL NULL
Which is fine and dandy but does anyone have any better ideas?
I've fiddled around with
SELECT JulianDate, AVG(RefETO)
OVER (ORDER BY JulianDate ROWS BETWEEN 28 PRECEDING AND CURRENT ROW)
FROM Cimis
and variations on that but haven't got anywhere
I agree with PM 77-1 in the comments. Conditional aggregation is the way to go.
Try this:
Declare #Stn INT, #Yr INT, #Period INT
Select #Stn = 80, #Yr=2014, #Period=28
SELECT Period, RefETo/RefEToDays AS RefETo, RefETo1/RefETo1Days AS RefETo1, Precipitation
FROM (
SELECT
ROUNDUP(JulianDate/#Period,0) AS Period,
SUM(
RefETo
) AS RefETo,
SUM(
CASE WHEN DATEPART(Year, DateCollected) = #Yr THEN RefETo ELSE 0 END
) AS RefETo1,
COUNT(*)
AS RefEToDays,
SUM(
CASE WHEN DATEPART(Year, DateCollected) = #Yr THEN 1 ELSE 0 END
) AS RefETo1Days,
SUM(
CASE WHEN DATEPART(Year, DateCollected) = #Yr Then Precipitation ELSE 0 END
) AS Precipitation
FROM Cimis
WHERE StationNo = #Stn
GROUP BY ROUNDUP(JulianDate/#Period,0)
) c
I need to get the average of RefETo over that five years and the
average of the last year, plus the sum of precipitation over the last
year.
From where I sit, this is a perfect example of a requirement that should be implemented in the report writer, not in the dbms.
I would not do this in a single SQL statement.
Yes! There definitely is a better, and more perforrmant, means to this end.
First be sure you either have a NUMBERS table defined in your database, or are familiar with how to generate one dynamically using a CTE (Common Table Expression):
The main query now becomes:
DECLARE #NumPeriods as INT = 365 / #Period;
WITH
-- vvv BEGIN Needed only in absence of a NUMBERS table
E1(N) as (
SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1) )E1(N)
),
E2(N) as ( SELECT 1 FROM E1 a CROSS JOIN E1 b ),
E4(N) as ( SELECT 1 FROM E2 a CROSS JOIN E2 b ),
-- repeated for E8, E16, as needed
-- ^^^ END Needed only in absence of a NUMBERS table
Tally(N) as (
SELECT TOP(#NumPeriods) -- This is sufficient for our purposes
ROW_NUMBER() OVER (ORDER BY N) AS N
FROM E4
UNION ALL
SELECT 0
)
INSERT INTO #AvgTable(Period, RefETo, RefETo1, Precipitation)
SELECT
p.Period
,AVG(RefETo) as RefETo
,SUM(case when DATEPART(Year, DateCollected) = #yr then RefETo else 0 end)
/SUM(case when DATEPART(Year, DateCollected) = #yr then 1 else 0 end)
as RefETo1
,SUM(case when DATEPART(Year, DateCollected) = #yr then Precipitation
else 0 end)
as Precipitation
FROM Cimis
JOIN (
SELECT
N, N + 1 as Period
,(N*#Period) + 1 as StartDate
,CASE WHEN N * #Period > 363 THEN #Period - 1 ELSE (N + 1) * #Period END AS EndDate
FROM Tally
) p
ON JulianDate BETWEEN p.StartDate AND p.EndDate
OR ((JulianDate > 364 OR JulianDate < #Period - 1) AND p.N * #Period > 363)
WHERE StationNo = #Stn
AND RefETO >= 0
GROUP BY p.Period
PRDER BY p.Period
I believe you will find this implementation to be vastly faster on large data sets than your current implementation.
This I think is the final version based on Jim V's code. The last period is handled with a UNION.
DECLARE #Stn INT, #Yr INT, #Period INT
SELECT #Stn = 80, #Yr=2014, #Period=28
SELECT Period, RefETo, RefETo1, Precipitation
FROM (
SELECT CEILING(JulianDate / #Period) AS Period, AVG(RefETo) AS RefETo,
AVG(CASE WHEN DATEPART(Year, DateCollected) = #Yr THEN RefETo ELSE NULL END) AS RefETo1,
SUM(CASE WHEN DATEPART(Year, DateCollected) = #Yr Then Precipitation ELSE 0 END) AS
Precipitation
FROM Cimis WHERE StationNo = #Stn AND JulianDate < 365
GROUP BY CEILING(JulianDate / #Period)) C
UNION SELECT 366 / #Period + 1 AS Period, AVG(RefETo) AS RefETo,
(SELECT AVG(RefETo) FROM CIMIS
WHERE DATEPART(Year, DateCollected) = #Yr AND JulianDate > 364) AS RefETo1,
(SELECT SUM(Precipitation) FROM CIMIS
WHERE DATEPART(Year, DateCollected) = #Yr AND JulianDate > 364) AS Precipitation
FROM Cimis WHERE StationNo = #Stn AND (JulianDate > 364 OR JulianDate < #Period - 1)
ORDER BY Period

Get day of the week in month (2nd Tuesday, etc.)

I need an algorithm for calculating the number of the day of the week in the month. Like 1st Friday of the month, 3rd Monday of the month, etc.)
Any ideas are appreciated.
Here is the final result:
declare #dt date = GetDate()
declare #DayOfWeek tinyint = datepart(weekday,#dt)
declare #DayOfMonth smallint = day(#dt)
declare #FirstDayOfMonth date = dateadd(month,datediff(month,0,#dt),0)
declare #DayOfWeekInMonth tinyint = #DayOfMonth / 7 + 1 -
(case when day(#FirstDayOfMonth) > day(#dt) then 1 else 0 end)
declare #Suffix varchar(2) =
case
when #DayOfWeekInMonth = 1 then 'st'
when #DayOfWeekInMonth = 2 then 'nd'
when #DayOfWeekInMonth = 3 then 'rd'
when #DayOfWeekInMonth > 3 then 'th'
end
select
cast(#DayOfWeekInMonth as varchar(2))
+ #Suffix
+ ' '
+ datename(weekday,#Dt)
+ ' of '
+ datename(month,#dt)
+ ', '
+ datename(year,#Dt)
PS: And if you can think of a better way to state the problem, please do.
Followint code will give you 1st Wednesday of April 2014 for today:
SELECT cast((DATEPART(d, GETDATE() - 1) / 7) + 1 as varchar(12))
+ 'st ' + DATENAME(WEEKDAY, getdate()) + ' of ' +
DATENAME(month, getdate()) + ' ' + DATENAME(year, getdate());
For any date use the code below. It gives 5th Tuesday of April 2014 for #mydate = '2014-04-29' in the example:
DECLARE #mydate DATETIME;
SET #mydate = '2014-04-29';
SELECT
case
when DATEPART(d, #mydate) = 1 then cast((DATEPART(d, #mydate ) / 7) + 1 as varchar(12))
else cast((DATEPART(d, #mydate - 1) / 7) + 1 as varchar(12))
end
+
case
when (DATEPART(d, #mydate - 1) / 7) + 1 = 1 then 'st '
when (DATEPART(d, #mydate - 1) / 7) + 1 = 2 then 'nd '
when (DATEPART(d, #mydate - 1) / 7) + 1 = 3 then 'rd '
else 'th '
end
+ DATENAME(WEEKDAY, #mydate) + ' of ' +
DATENAME(month, #mydate) + ' ' + DATENAME(year, #mydate) as [Long Date Name]
Okeeeey my tuuuurn ,
Please rate my answer Metaphor hhh, Here's the cooode :
declare #v_month nvarchar(2) = '04'
,#v_annee nvarchar(4) = '2014'
declare #v_date date = convert(date,#v_annee+'-'+#v_month+'-01')
declare #v_date_2 date = dateadd(M,1,#v_date)
if OBJECT_ID('temp') is not null
drop table temp
create table temp(_date date, _DayOfMonth nvarchar(20), _order int)
while (#v_date<#v_date_2)
begin
set #v_date =#v_date;
WITH _DayOfWeek AS (
SELECT 1 id, 'monday' Name UNION ALL
SELECT 2 id, 'tuesday' Name UNION ALL
SELECT 3 id, 'wednesday' Name UNION ALL
SELECT 4 id, 'thursday' Name UNION ALL
SELECT 5 id, 'friday' Name UNION ALL
SELECT 6 id, 'saturday' Name UNION ALL
SELECT 7 id, 'sunday' Name)
insert into temp(_date,_DayOfMonth)
SELECT
#v_date
,(select Name from _DayOfWeek where id = DATEPART(WEEKDAY,#v_date))
SET #v_date = DATEADD(DAY,1,#v_date)
END
UPDATE tmp1
SET _order = _order_2
FROM temp tmp1
INNER JOIN
(SELECT *, ROW_NUMBER() OVER(PARTITION BY _DayOfMonth ORDER BY _date ASC) AS _order_2 FROM temp) tmp2
ON tmp1._date = tmp2._date
SELECT * FROM temp
SELECT *
FROM temp
WHERE _DayOfMonth = 'thursday'
AND _order = 3
I hope this will help you :)
Good Luck
OK, here's what I came up with, I'll +1 everyone who answered anyway:
declare #dt date = GetDate()
declare #DayOfWeek tinyint = datepart(weekday,#dt)
declare #DayOfMonth smallint = day(#dt)
declare #FirstDayOfMonth date = dateadd(month,datediff(month,0,#dt),0)
declare #DayOfWeekInMonth tinyint =
#DayOfMonth / 7 + 1
- (case when day(#FirstDayOfMonth) > day(#dt) then 1 else 0 end)
declare #Suffix varchar(2) =
case
when #DayOfWeekInMonth = 1 then 'st'
when #DayOfWeekInMonth = 2 then 'nd'
when #DayOfWeekInMonth = 3 then 'rd'
when #DayOfWeekInMonth > 3 then 'th'
end
select
cast(#DayOfWeekInMonth as varchar(2))
+ #Suffix
+ ' '
+ datename(weekday,#Dt)
+ ' of '
+ datename(month,#dt)
+ ', '
+ datename(year,#Dt)
declare #dt date = getdate()
declare #DayOfMonth smallint = datepart(d, #dt)
declare #Suffix varchar(2) =
case
when floor((#DayOfMonth - 1) / 7.0) = 0 then 'st' -- implies there were no such days previously in the month
when floor((#DayOfMonth - 1) / 7.0) = 1 then 'nd'
when floor((#DayOfMonth - 1) / 7.0) = 2 then 'rd'
else 'th'
end
select cast(floor((#DayOfMonth - 1) / 7.0) + 1 as varchar(1)) + #Suffix +
' ' + datename(weekday, #dt) + ' of ' + datename(month, #dt) +
', ' + datename(year, #dt)
DECLARE #dt DATETIME
SET #dt = DATEADD(d, 6, GETDATE())
SELECT #dt,
CAST((DAY(#dt) / 7) + CASE WHEN DATEPART(weekday, #dt) >= DATEPART(weekday, CAST(MONTH(#dt) AS NVARCHAR) + '/01/' + CAST(YEAR(#dt) AS NVARCHAR)) THEN 1 ELSE 0 END AS NVARCHAR)
+ '' + CASE (DAY(#dt) / 7) + CASE WHEN DATEPART(weekday, #dt) >= DATEPART(weekday, CAST(MONTH(#dt) AS NVARCHAR) + '/01/' + CAST(YEAR(#dt) AS NVARCHAR)) THEN 1 ELSE 0 END
WHEN 1 THEN N'st'
WHEN 2 THEN N'nd'
WHEN 3 THEN N'rd'
ELSE N'th'
END
+ ' ' + DATENAME(dw, #dt)
+ ' of ' + DATENAME(M, #dt)
+ ', ' + CAST(YEAR(#dt) AS NVARCHAR)
Result is a single SELECT (provided the assignment of #dt happened earlier) but is, essentially, the same logic as yours.
This following code will give you DATE for any day of the week in any month or year that you specify. All the variables that I have are to reduce repeating logic to improve code speed.
This code gives you date for 1st Monday in February in 2013
DECLARE #DayNumber INT = 1
,#DayWeekNumber INT = 2
,#MonthNumber INT = 2
,#YearNumber INT = 2013
,#FoM DATE
,#FoMWD INT;
SET #FoM = DATEFROMPARTS(#YearNumber,#MonthNumber,1)
SET #fomwd = DATEPART(WEEKDAY, #FoM);
SELECT CASE WHEN #fomwd = #DayWeekNumber THEN DATEADD(WEEK, #DayNumber - 1, #FoM)
WHEN #fomwd < #DayWeekNumber THEN DATEADD(DAY, #DayWeekNumber - #fomwd, DATEADD(WEEK, #DayNumber - 1, #FoM))
WHEN #fomwd > #DayWeekNumber THEN DATEADD(DAY, #DayWeekNumber - #fomwd, DATEADD(WEEK, #DayNumber, #FoM))
END AS DateOfDay;

if time is more than 60 minutes then show in hours in sql server

I have a stored procedure like :
ALTER procedure [dbo].[IBS_Podiumsummryaveragetime]
#locid integer=null
as
begin
set nocount on;
select avg( datediff(mi,t.dtime, t.DelDate )) as Avgstaytime,
avg( datediff(mi,t.dtime, t.PAICdate )) as Avgparkingtime,
avg( datediff(mi,t.Paydate, t.DelDate )) as AvgDelivarytime
from (select top 10 * from transaction_tbl where locid=#locid and dtime >= getdate()-7 order by transactID desc ) t
end
if average time i am getting more than 60 minutes then i want to show in hours..how i can do this?
any help is appreciable??
I couldn't type this in the comments so I've added it as an answer:
SELECT
CASE
WHEN avg( datediff(mi,t.dtime, t.DelDate )) <= 60
THEN CAST(avg( datediff(mi,t.dtime, t.DelDate )), varchar)
ELSE
CAST(avg( datediff(mi,t.dtime, t.DelDate )) / 60, varchar) + ":" + CAST(avg( datediff(mi,t.dtime, t.DelDate )) % 60, varchar)
END
Variations on #I.K.'s theme's (which I up voted)
SELECT CASE WHEN avg(datediff(mi,t.dtime, t.DelDate)) > 60
THEN CAST(avg(datediff(mi,t.dtime, t.DelDate))/60 AS VARCHAR(3)) + ':'
ELSE ''
END
+ CAST(avg(datediff(mi,t.dtime, t.DelDate)) % 60 AS VARCHAR(2))
--OR
SELECT CASE WHEN avg(datediff(hour,t.dtime, t.DelDate)) > 0
THEN CAST(avg(datediff(hour,t.dtime, t.DelDate)) AS VARCHAR(3)) + ':'
ELSE ''
END
+ CAST(avg(datediff(mi,t.dtime, t.DelDate)) % 60 AS VARCHAR(2))

tsql very optimized UDF

What is the best code (UDF) to calculate the age given birthday? Very optimized code. I can write the date related code just to make sure what best answer I get from best minds in the industry.
Would like to calculate based on days.
The following function gives very reasonable answers for all the date ranges that I've commented in this thread. You will notice that some values may be off by 1 day, but this is an artifact of how months are counted due to them being varying length. February always gives the most trouble being the shortest. But it should never be off by more than 1 day from dead reckoning (and even that comes down to semantics of how you are supposed to count the months, and is still a valid answer). Sometimes, it may give a number of days when you might have expected 1 month, but again it is still a reasonable answer.
The function, using only date math instead of string operations until the very end, should also yield very good performance when stacked up against any other function that returns calculations that are similarly accurate.
CREATE FUNCTION dbo.AgeInYMDFromDates(
#FromDate datetime,
#ToDate datetime
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (
SELECT
Convert(varchar(11), AgeYears) + 'y '
+ Convert(varchar(11), AgeMonths) + 'm '
+ Convert(varchar(11), DateDiff(day, DateAdd(month, AgeMonths,
DateAdd(year, AgeYears, #FromDate)), #ToDate)
) + 'd' Age
FROM (
SELECT
DateDiff(year, #FromDate, #ToDate)
- CASE WHEN Month(#FromDate) * 32 + Day(#FromDate)
> Month(#ToDate) * 32 + Day(#ToDate) THEN 1 ELSE 0 END AgeYears,
(DateDiff(month, #FromDate, #ToDate)
- CASE WHEN Day(#FromDate) > Day(#ToDate) THEN 1 ELSE 0 END) % 12 AgeMonths
) X
);
Use like so (SQL 2008 script):
SELECT
FromDate,
ToDate,
ExpectedYears,
ExpectedMonths,
ExpectedDays,
(SELECT TOP 1 Age FROM dbo.AgeInYMDFromDates(FromDate, ToDate)) Age
FROM
(
VALUES
(Convert(datetime, '20120201'), Convert(datetime, '20120301'), 0, 1, 0),
('20120228', '20120328', 0, 1, 0),
('20120228', '20120331', 0, 1, 3),
('20120228', '20120327', 0, 0, 27),
('20120801', '20120802', 0, 0, 1),
('20120131', '20120301', 0, 1, 2),
('19920507', '20120506', 19, 11, 29),
('19920507', '20120507', 20, 0, 0)
) X (FromDate, ToDate, ExpectedYears, ExpectedMonths, ExpectedDays)
Because it is an inline function, it can be inserted into the execution plan of the query and will perform the best possible. If you convert it to a scalar-returning function (so you don't have to use (SELECT Age FROM func) then you will get worse performance. The WITH SCHEMABINDING directive can help because it precalculates that the function makes no data access to any tables rather than having to check it at runtime.
Here are the results of the above execution:
FromDate ToDate ExpectedYears ExpectedMonths ExpectedDays Age
---------- ---------- ------------- -------------- ------------ -----------
2012-02-01 2012-03-01 0 1 0 0y 1m 0d
2012-02-28 2012-03-28 0 1 0 0y 1m 0d
2012-02-28 2012-03-31 0 1 3 0y 1m 3d
2012-02-28 2012-03-27 0 0 27 0y 0m 28d
2012-08-01 2012-08-02 0 0 1 0y 0m 1d
2012-01-31 2012-03-01 0 1 2 0y 1m 1d
1992-05-07 2012-05-06 19 11 29 19y 11m 29d
1992-05-07 2012-05-07 20 0 0 20y 0m 0d
Enjoy!
Try This.
CREATE TABLE patient(PatName varchar(100), DOB date, Age varchar(100))
INSERT INTO patient
VALUES ('d','06/02/2011',NULL)--,('b','07/10/1947',NULL),('c','12/21/1982',NULL)
;WITH CTE(PatName,DOB,years,months,days) AS
(SELECT PatName, DOB, DATEDIFF(yy,DOB,getdate()), DATEDIFF(mm,DOB,getdate()),
DATEDIFF(dd,DOB,getdate())
FROM patient)
SELECT PatName, DOB,
CAST(months/12 as varchar(5)) + ' Years' +
CAST((months % 12) as varchar(5)) + ' month/s ' +
CAST( CASE WHEN DATEADD(MM,(months % 12), DATEADD(YY,(months/12),DOB)) <= GETDATE()
THEN DATEDIFF(dd,DATEADD(MM,(months % 12), DATEADD(YY,(months/12),DOB)),GETDATE())
ELSE DAY(getdate())
END as varchar(5))+' days' AS Age
FROM CTE
Performance of scalar functions is usually not very good, to do what you ask I would write it as a table valued function. A table valued function has the benefit that it is inlined properly.
Other forms of the query will not make a huge difference as it is the calls to the function which eat up the time.
CREATE FUNCTION dbo.fn_age(#birthdate datetime, #current_date datetime)
RETURNS TABLE
WITH SCHEMABINDING
AS
return (
select CAST(DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END AS varchar(3)) + ' y, '
+ CAST(DATEDIFF(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate),
#current_date) AS varchar(2)) + ' m, '
+ CAST(DATEDIFF(day, DATEADD(month
, datediff(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
, #current_date)
, dateadd(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
)
, #current_date) AS varchar(2)) + ' d' as [Age]
)
GO
This function has to be called like this:
SELECT Age = (SELECT Age FROM dbo.fn_age(birthDate, current_timestamp))
FROM Person
Comparison with other alternatives
When writing this problem as a normal scalar function I would create something like this:
CREATE FUNCTION dbo.fn_age_slow(#birthdate datetime, #current_date datetime )
RETURNS VARCHAR(10)
WITH SCHEMABINDING
AS
begin
return CAST(DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END AS varchar(3)) + ' y, '
+ CAST(DATEDIFF(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate),
#current_date) AS varchar(2)) + ' m, '
+ CAST(DATEDIFF(day, DATEADD(month
, datediff(MONTH, DATEADD(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
, #current_date)
, dateadd(year
, /* the year calculation*/ DATEDIFF(year, #birthdate, #current_date) - CASE WHEN((MONTH(#birthdate) * 100 + DAY(#birthdate)) > (MONTH(#current_date) * 100 + DAY(#current_date))) THEN 1 ELSE 0 END /* end of the year calculation*/
, #birthdate)
)
, #current_date) AS varchar(2)) + ' d'
end
GO
As you can see it does exactly the same as the table valued function. (it is also schema bound which makes the functions faster in some cases)
When running the following script against the first function (on my pc)
declare #a varchar(10) = ''
, #d datetime = '20120101'
, #i int = 1
, #begin datetime
select #begin = CURRENT_TIMESTAMP
while #i < 1000000
begin
select #a = Age
, #d = #begin - #i%1000
, #i += 1
from dbo.fn_age(#d, #begin)
end
select CURRENT_TIMESTAMP - #begin
GO
==> 00:00:07.920
declare #a varchar(10) = ''
, #d datetime = '19500101'
, #i int = 1
, #begin datetime
select #begin = CURRENT_TIMESTAMP
while #i < 1000000
begin
select #a = dbo.fn_age_slow(#d, #begin)
, #d = #begin - #i%1000
, #i += 1
end
select CURRENT_TIMESTAMP - #begin
==> 00:00:14.023
This is in no way a proper benchmark but it should give you an idea about the performance difference.

Best way to convert DateTime to "n Hours Ago" in SQL

I wrote a SQL function to convert a datetime value in SQL to a friendlier "n Hours Ago" or "n Days Ago" etc type of message. And I was wondering if there was a better way to do it.
(Yes I know "don't do it in SQL" but for design reasons I have to do it this way).
Here is the function I've written:
CREATE FUNCTION dbo.GetFriendlyDateTimeValue
(
#CompareDate DateTime
)
RETURNS nvarchar(48)
AS
BEGIN
DECLARE #Now DateTime
DECLARE #Hours int
DECLARE #Suff nvarchar(256)
DECLARE #Found bit
SET #Found = 0
SET #Now = getDate()
SET #Hours = DATEDIFF(MI, #CompareDate, #Now)/60
IF #Hours <= 1
BEGIN
SET #Suff = 'Just Now'
SET #Found = 1
RETURN #Suff
END
IF #Hours < 24
BEGIN
SET #Suff = ' Hours Ago'
SET #Found = 1
END
IF #Hours >= 8760 AND #Found = 0
BEGIN
SET #Hours = #Hours / 8760
SET #Suff = ' Years Ago'
SET #Found = 1
END
IF #Hours >= 720 AND #Found = 0
BEGIN
SET #Hours = #Hours / 720
SET #Suff = ' Months Ago'
SET #Found = 1
END
IF #Hours >= 168 AND #Found = 0
BEGIN
SET #Hours = #Hours / 168
SET #Suff = ' Weeks Ago'
SET #Found = 1
END
IF #Hours >= 24 AND #Found = 0
BEGIN
SET #Hours = #Hours / 24
SET #Suff = ' Days Ago'
SET #Found = 1
END
RETURN Convert(nvarchar, #Hours) + #Suff
END
As you say, I probably wouldn't do it in SQL, but as a thought exercise have a MySQL implementation:
CASE
WHEN compare_date between date_sub(now(), INTERVAL 60 minute) and now()
THEN concat(minute(TIMEDIFF(now(), compare_date)), ' minutes ago')
WHEN datediff(now(), compare_date) = 1
THEN 'Yesterday'
WHEN compare_date between date_sub(now(), INTERVAL 24 hour) and now()
THEN concat(hour(TIMEDIFF(NOW(), compare_date)), ' hours ago')
ELSE concat(datediff(now(), compare_date),' days ago')
END
Based on a similar sample seen on the MySQL Date and Time manual pages
In Oracle:
select
CC.MOD_DATETIME,
'Last modified ' ||
case when (sysdate - cc.mod_datetime) < 1
then round((sysdate - CC.MOD_DATETIME)*24) || ' hours ago'
when (sysdate - CC.MOD_DATETIME) between 1 and 7
then round(sysdate-CC.MOD_DATETIME) || ' days ago'
when (sysdate - CC.MOD_DATETIME) between 8 and 365
then round((sysdate - CC.MOD_DATETIME) / 7) || ' weeks ago'
when (sysdate - CC.MOD_DATETIME) > 365
then round((sysdate - CC.MOD_DATETIME) / 365) || ' years ago'
end
from
customer_catalog CC
My attempt - this is for MS SQL. It supports 'ago' and 'from now', pluralization and it doesn't use rounding or datediff, but truncation -- datediff gives 1 month diff between 8/30 and 9/1 which is probably not what you want. Rounding gives 1 month diff between 9/1 and 9/16. Again, probably not what you want.
CREATE FUNCTION dbo.GetFriendlyDateTimeValue( #CompareDate DATETIME ) RETURNS NVARCHAR(48) AS BEGIN
declare #s nvarchar(48)
set #s='Now'
select top 1 #s=convert(nvarchar,abs(n))+' '+s+case when abs(n)>1 then 's' else '' end+case when n>0 then ' ago' else ' from now' end from (
select convert(int,(convert(float,(getdate()-#comparedate))*n)) as n, s from (
select 1/365 as n, 'Year' as s union all
select 1/30, 'Month' union all
select 1, 'Day' union all
select 7, 'Week' union all
select 24, 'Hour' union all
select 24*60, 'Minute' union all
select 24*60*60, 'Second'
) k
) j where abs(n)>0 order by abs(n)
return #s
END
Your code looks functional. As for a better way, that is going to get subjective. You might want to check out this page as it deals with time spans in SQL.
How about this? You could expand this pattern to do "years" messages, and you could put in a check for "1 day" or "1 hour" so it wouldn't say "1 days ago"...
I like the CASE statement in SQL.
drop function dbo.time_diff_message
GO
create function dbo.time_diff_message (
#input_date datetime
)
returns varchar(200)
as
begin
declare #msg varchar(200)
declare #hourdiff int
set #hourdiff = datediff(hour, #input_date, getdate())
set #msg = case when #hourdiff < 0 then ' from now' else ' ago' end
set #hourdiff = abs(#hourdiff)
set #msg = case when #hourdiff > 24 then convert(varchar, #hourdiff/24) + ' days' + #msg
else convert(varchar, #hourdiff) + ' hours' + #msg
end
return #msg
end
GO
select dbo.time_diff_message('Dec 7 1941')
Thanks for the various code posted above.
As Hafthor pointed out there are limitations of the original code to do with rounding. I also found that some of the results his code kicked out didn't match with what I'd expect e.g. Friday afternoon -> Monday morning would show as '2 days ago'. I think we'd all call that 3 days ago, even though 3 complete 24 hour periods haven't elapsed.
So I've amended the code (this is MS SQL). Disclaimer: I am a novice TSQL coder so this is quite hacky, but works!!
I've done some overrides - e.g. anything up to 2 weeks is expressed in days. Anything over that up to 2 months is expressed in weeks. Anything over that is in months etc. Just seemed like the intuitive way to express it.
CREATE FUNCTION [dbo].[GetFriendlyDateTimeValue]( #CompareDate DATETIME ) RETURNS NVARCHAR(48) AS BEGIN
declare #s nvarchar(48)
set #s='Now'
select top 1 #s=convert(nvarchar,abs(n))+' '+s+case when abs(n)>1 then 's' else '' end+case when n>0 then ' ago' else ' from now' end from (
select convert(int,(convert(float,(getdate()-#comparedate))*n)) as n, s from (
select 1/365 as n, 'year' as s union all
select 1/30, 'month' union all
select 1/7, 'week' union all
select 1, 'day' union all
select 24, 'hour' union all
select 24*60, 'minute' union all
select 24*60*60, 'second'
) k
) j where abs(n)>0 order by abs(n)
if #s like '%days%'
BEGIN
-- if over 2 months ago then express in months
IF convert(nvarchar,DATEDIFF(MM, #CompareDate, GETDATE())) >= 2
BEGIN
select #s = convert(nvarchar,DATEDIFF(MM, #CompareDate, GETDATE())) + ' months ago'
END
-- if over 2 weeks ago then express in weeks, otherwise express as days
ELSE IF convert(nvarchar,DATEDIFF(DD, #CompareDate, GETDATE())) >= 14
BEGIN
select #s = convert(nvarchar,DATEDIFF(WK, #CompareDate, GETDATE())) + ' weeks ago'
END
ELSE
select #s = convert(nvarchar,DATEDIFF(DD, #CompareDate, GETDATE())) + ' days ago'
END
return #s
END
The posts above gave me some good ideas so here is another function for anyone using SQL Server 2012.
CREATE FUNCTION [dbo].[FN_TIME_ELAPSED]
(
#TIMESTAMP DATETIME
)
RETURNS VARCHAR(50)
AS
BEGIN
RETURN
(
SELECT TIME_ELAPSED =
CASE
WHEN #TIMESTAMP IS NULL THEN NULL
WHEN MINUTES_AGO < 60 THEN CONCAT(MINUTES_AGO, ' minutes ago')
WHEN HOURS_AGO < 24 THEN CONCAT(HOURS_AGO, ' hours ago')
WHEN DAYS_AGO < 365 THEN CONCAT(DAYS_AGO, ' days ago')
ELSE CONCAT(YEARS_AGO, ' years ago') END
FROM ( SELECT MINUTES_AGO = DATEDIFF(MINUTE, #TIMESTAMP, GETDATE()) ) TIMESPAN_MIN
CROSS APPLY ( SELECT HOURS_AGO = DATEDIFF(HOUR, #TIMESTAMP, GETDATE()) ) TIMESPAN_HOUR
CROSS APPLY ( SELECT DAYS_AGO = DATEDIFF(DAY, #TIMESTAMP, GETDATE()) ) TIMESPAN_DAY
CROSS APPLY ( SELECT YEARS_AGO = DATEDIFF(YEAR, #TIMESTAMP, GETDATE()) ) TIMESPAN_YEAR
)
END
GO
And the implementation:
SELECT TIME_ELAPSED = DBO.FN_TIME_ELAPSED(AUDIT_TIMESTAMP)
FROM SOME_AUDIT_TABLE
CASE WHEN datediff(SECOND,OM.OrderDate,GETDATE()) < 60 THEN
CONVERT(NVARCHAR(MAX),datediff(SECOND,OM.OrderDate,GETDATE())) +' seconds ago'
WHEN datediff(MINUTE,OM.OrderDate,GETDATE()) < 60 THEN
CONVERT(NVARCHAR(MAX),datediff(MINUTE,OM.OrderDate,GETDATE())) +' minutes ago'
WHEN datediff(HOUR,OM.OrderDate,GETDATE()) < 24 THEN
CONVERT(NVARCHAR(MAX),datediff(HOUR,OM.OrderDate,GETDATE())) +' hours ago'
WHEN datediff(DAY,OM.OrderDate,GETDATE()) < 8 THEN
CONVERT(NVARCHAR(MAX),datediff(DAY,OM.OrderDate,GETDATE())) +' Days ago'
ELSE FORMAT(OM.OrderDate,'dd/MM/yyyy hh:mm tt') END AS TimeStamp