Question

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
Was it helpful?

Solution

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

OTHER TIPS

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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top