Datediff between dates, but exclude extra leap year day

  • I'm trying to figure out what the best way to calculate the number of days between two dates, but I want to exclude the extra day in leap years.  What I've googled says to use a date table for this.  This seems like overkill to me when most of the time a simple Datdiff is going to get the right number of days.

    So I thought I would throw this idea out and see what kind of response I get.

    Should I just use Datediff, then somehow determine if the leap day is within the range, then subtract 1?  I doubt I would ever have the situation where the date range would be over more than one leap year day.

    DECLARE @Eff_DateDATE;
    DECLARE @Exp_DateDATE;

    SET @Eff_Date = '01/01/2020';
    SET @Exp_Date = '01/01/2021';

    SELECT DATEDIFF(d, @Eff_Date, @Exp_Date) AS Date_diff;
    -- Results 366
    -- I would want 365

    SET @Eff_Date = '01/01/2020';
    SET @Exp_Date = '07/01/2020';

    SELECT DATEDIFF(d, @Eff_Date, @Exp_Date) AS Date_diff;
    -- Results 182
    -- I would want 181

    This calculation would need to happen in a batch process at night.  Calculating the number of days in an insurance policy term.

    Thanks in advance for any thoughts you have on this.

    -------------------------------------------------------------
    we travel not to escape life but for life not to escape us
    Don't fear failure, fear regret.

  • Maybe I just needed to post something to get the brain going.

    Here is what I came up with.  I borrowed some code I found at https://www.mssqltips.com/sqlservertip/1527/sql-server-function-to-determine-a-leap-year/

    DECLARE @Eff_DateDATE;
    DECLARE @Exp_DateDATE;

    SET @Eff_Date = '01/01/2020';
    SET @Exp_Date = '01/01/2021';

    SELECT DATEDIFF(d, @Eff_Date, @Exp_Date) AS Date_diff;
    -- Results 366
    -- I would want 365

    SET @Eff_Date = '01/01/2020';
    SET @Exp_Date = '07/01/2020';

    SELECT DATEDIFF(d, @Eff_Date, @Exp_Date) AS Date_diff;
    -- Results 182
    -- I would want 181
    /*
    -- determine if a year is a leap year
    SELECT
    CASE DATEPART(mm, DATEADD(dd, 1, CAST((CAST(@year AS VARCHAR(4)) + '0228') AS DATE)))
    WHEN 2 THEN 1
    ELSE
    0
    END
    ;
    */

    SELECT DATEDIFF(d, @Eff_Date, @Exp_Date) -
    (CASEWHEN CAST((CAST(YEAR(@Eff_Date) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THENCASE DATEPART(mm, DATEADD(dd, 1, CAST((CAST(YEAR(@Eff_Date) AS VARCHAR(4)) + '0228') AS DATE)))
    WHEN 2 THEN 1
    ELSE
    0
    END
    WHEN CAST((CAST(YEAR(@Exp_Date) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THENCASE DATEPART(mm, DATEADD(dd, 1, CAST((CAST(YEAR(@Exp_Date) AS VARCHAR(4)) + '0228') AS DATE)))
    WHEN 2 THEN 1
    ELSE
    0
    END
    ELSE
    0
    END) AS Date_diff
    ;

    This seems to do the trick in initial testing, I'll do a little more to be sure.

    Would you create this or part of it(looking at the last bit of SQL posted) as a function?

    -------------------------------------------------------------
    we travel not to escape life but for life not to escape us
    Don't fear failure, fear regret.

  • Of course, if I would read through the comments(at the link I posted), Jeff Moden would have a simpler way to determine a leap year.

    SELECT 
    CAST(DATEDIFF(d, @Eff_Date, @Exp_Date) AS INT) -
    CAST(CASEWHEN CAST((CAST(YEAR(@Eff_Date) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THEN ISDATE(CAST(YEAR(@Eff_Date) AS VARCHAR(4))+'0229')
    WHEN CAST((CAST(YEAR(@Exp_Date) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THEN ISDATE(CAST(YEAR(@Eff_Date) AS VARCHAR(4))+'0229')
    ELSE
    0
    END AS INT) AS Date_diff
    ;

    Updated SQL, needed the CAST as Varchar on the Year.

    Thanks Jeff.

    • This reply was modified 4 years, 11 months ago by  below86.

    -------------------------------------------------------------
    we travel not to escape life but for life not to escape us
    Don't fear failure, fear regret.

  • If you install this table valued function you could do it like this:

    DECLARE @Eff_DateDATE;
    DECLARE @Exp_DateDATE;

    SET @Eff_Date = '01/01/2020';
    SET @Exp_Date = '01/01/2021';

    SELECT COUNT(Value) AS Days
    FROM dbo.DateRange(@Eff_Date,@Exp_Date,'dd',1)
    WHERE DATEPART(mm, Value) <> 2
    OR DATEPART(dd, Value) <> 29

    PS: You might need to subtract 1 from the result as it includes both the start and end intervals.

  • Thanks for the feedback and info Jonathan, I'll take a look at it.

    Found lots of holes in the prior SQL I posted, here is my revised SQL:

    DECLARE @Eff_DateDATE;
    DECLARE @Exp_DateDATE;

    SET @Eff_Date = '12/01/2019';
    SET @Exp_Date = '05/01/2021';

    SELECT DATEDIFF(d, @Eff_Date, @Exp_Date) AS Date_diff;
    -- Results 517
    -- I would want 516

    SELECT
    CAST(DATEDIFF(d, @Eff_Date, @Exp_Date) AS INT) -
    CAST(CASEWHEN CAST((CAST(YEAR(@Eff_Date) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THENCASEISDATE(CAST(YEAR(@Eff_Date) AS VARCHAR(4))+'0229')
    WHEN 1 THEN1
    ELSE
    CASEWHEN CAST((CAST(YEAR(@Exp_Date) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THEN ISDATE(CAST(YEAR(@Exp_Date) AS VARCHAR(4))+'0229')
    ELSE
    0
    END
    END
    WHEN CAST((CAST(YEAR(@Exp_Date) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THENCASEWHEN ISDATE(CAST(YEAR(@Exp_Date) AS VARCHAR(4))+'0229') = 1
    THEN 1
    WHEN CAST((CAST((YEAR(@Eff_Date) +1) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THEN ISDATE(CAST((YEAR(@Eff_Date) + 1) AS VARCHAR(4))+'0229')
    ELSE
    0
    END
    WHEN CAST((CAST((YEAR(@Eff_Date) +1) AS VARCHAR(4)) + '0228') AS DATE) BETWEEN @Eff_Date AND @Exp_Date
    THEN ISDATE(CAST((YEAR(@Eff_Date) + 1) AS VARCHAR(4))+'0229')
    ELSE
    0
    END AS INT) AS Date_diff
    ;
    -- Results 516

    I know we can have 18 month policies, I don't think they can be longer than that.

    -------------------------------------------------------------
    we travel not to escape life but for life not to escape us
    Don't fear failure, fear regret.

  • Here is an option...

    Declare @startDate datetime = '2020-01-01'
    , @endDate datetime = '2021-01-01';

    With dates (y, StartDate, EndDate, Feb28)
    As (
    Select Top (year(@endDate) - year(@startDate) + 1)
    y
    , iif(@startDate > YearStart, @startDate, YearStart)
    , iif(@endDate < YearEnd, @endDate, YearEnd)
    , Feb28
    From (Values (0), (1), (2), (3), (4), (5), (6)) As n(n)
    Cross Apply (Values (year(@startDate) + n)) As y(y)
    Cross Apply (Values (
    datetimefromparts(y, 2, 28, 0, 0, 0, 0)
    , datetimefromparts(y, 1, 1, 0, 0, 0, 0)
    , datetimefromparts(y, 12, 31, 0, 0, 0, 0))) As yy(Feb28, YearStart, YearEnd)
    )
    Select *, Date_Diff = datediff(day, d.StartDate, d.EndDate)
    , Adjusted_Date_Diff = datediff(day, d.StartDate, d.EndDate)
    - iif(d.StartDate <= d.Feb28 And d.EndDate > d.Feb28, l.ly, 0)
    + iif(d.StartDate = d.EndDate, 1, 0)
    From dates As d
    Cross Apply (Values (iif((y % 400 = 0 Or y % 4 = 0)
    And y % 100 <> 0, 1, 0))) As l(ly);

    To get the totals - change it to:

     Select Date_Diff = sum(datediff(day, d.StartDate, d.EndDate))
    , Adjusted_Date_Diff = sum(datediff(day, d.StartDate, d.EndDate)
    - iif(d.StartDate <= d.Feb28 And d.EndDate > d.Feb28, l.ly, 0)
    + iif(d.StartDate = d.EndDate, 1, 0))
    From dates As d
    Cross Apply (Values (iif((y % 400 = 0 Or y % 4 = 0)
    And y % 100 <> 0, 1, 0))) As l(ly);

    Jeffrey Williams
    “We are all faced with a series of great opportunities brilliantly disguised as impossible situations.”

    ― Charles R. Swindoll

    How to post questions to get better answers faster
    Managing Transaction Logs

  • This will work for all dates between 1800 and 2400, it's more efficient than my previous proposed method

    DECLARE @Eff_DateDATE;
    DECLARE @Exp_DateDATE;

    SET @Eff_Date = '01/01/2020';
    SET @Exp_Date = '01/01/2021';

    SELECT DATEDIFF(dd,@Eff_Date,@Exp_Date)
    - (SELECT COUNT(*)
    FROM (VALUES ('18040229'),('18080229'),('18120229'),('18160229'),('18200229'),('18240229'),('18280229'),
    ('18320229'),('18360229'),('18400229'),('18440229'),('18480229'),('18520229'),('18560229'),('18600229'),('18640229'),
    ('18680229'),('18720229'),('18760229'),('18800229'),('18840229'),('18880229'),('18920229'),('18960229'),('19040229'),
    ('19080229'),('19120229'),('19160229'),('19200229'),('19240229'),('19280229'),('19320229'),('19360229'),('19400229'),
    ('19440229'),('19480229'),('19520229'),('19560229'),('19600229'),('19640229'),('19680229'),('19720229'),('19760229'),
    ('19800229'),('19840229'),('19880229'),('19920229'),('19960229'),('20000229'),('20040229'),('20080229'),('20120229'),
    ('20160229'),('20200229'),('20240229'),('20280229'),('20320229'),('20360229'),('20400229'),('20440229'),('20480229'),
    ('20520229'),('20560229'),('20600229'),('20640229'),('20680229'),('20720229'),('20760229'),('20800229'),('20840229'),
    ('20880229'),('20920229'),('20960229'),('21040229'),('21080229'),('21120229'),('21160229'),('21200229'),('21240229'),
    ('21280229'),('21320229'),('21360229'),('21400229'),('21440229'),('21480229'),('21520229'),('21560229'),('21600229'),
    ('21640229'),('21680229'),('21720229'),('21760229'),('21800229'),('21840229'),('21880229'),('21920229'),('21960229'),
    ('22040229'),('22080229'),('22120229'),('22160229'),('22200229'),('22240229'),('22280229'),('22320229'),('22360229'),
    ('22400229'),('22440229'),('22480229'),('22520229'),('22560229'),('22600229'),('22640229'),('22680229'),('22720229'),
    ('22760229'),('22800229'),('22840229'),('22880229'),('22920229'),('22960229'),('23040229'),('23080229'),('23120229'),
    ('23160229'),('23200229'),('23240229'),('23280229'),('23320229'),('23360229'),('23400229'),('23440229'),('23480229'),
    ('23520229'),('23560229'),('23600229'),('23640229'),('23680229'),('23720229'),('23760229'),('23800229'),('23840229'),
    ('23880229'),('23920229'),('23960229'),('24000229')) T(Feb29)
    WHERE T.Feb29 BETWEEN @Eff_Date AND @Exp_Date)

     

  • This can be made more dynamic with a function that gets the FEB29 days

    CREATE FUNCTION dbo.GetLeapYearDates(@Date1 AS date, @Date2 AS date)
    RETURNS TABLE WITH SCHEMABINDING
    AS
    RETURN
    WITH
    T(N) AS (SELECT N FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0)) AS X(N))
    , Nums AS (SELECT TOP(ABS(YEAR(@Date2) - YEAR(@Date1)) +1) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS rownum
    FROM T T1 -- Max = 64
    , T T2 -- Max = 4,096
    , T T3 -- Max = 262,144
    )
    SELECT DATEADD(DD, 59, DATEADD(YY, (SELECT MIN(YEAR(dt)) FROM (VALUES (@Date1), (@Date2)) AS v(dt)) + rownum -1901, 0)) AS LongFeb
    FROM Nums
    WHERE DAY(DATEADD(DD, 59, DATEADD(YY, (SELECT MIN(YEAR(dt)) FROM (VALUES (@Date1), (@Date2)) AS v(dt)) + rownum -1901, 0))) = 29;
    GO

     

    DECLARE @Eff_DateDATE;
    DECLARE @Exp_DateDATE;

    SET @Eff_Date = '2020-01-01';
    SET @Exp_Date = '2021-01-01';

    SELECT DATEDIFF(dd,@Eff_Date,@Exp_Date)
    - (SELECT COUNT(*)
    FROM dbo.GetLeapYearDates(@Eff_Date, @Exp_Date))
  • If you want a solution that can have any start/end date for any range - between 1753 and 9999:

    Declare @startDate datetime = '1753-01-01'
    , @endDate datetime = '9999-12-31';

    With t(n)
    As (
    Select t.n
    From (
    Values (0), (0), (0), (0), (0), (0), (0), (0), (0), (0)
    , (0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) As t(n)
    )
    , inputYears (y)
    As (
    Select Top (year(@endDate) - year(@startDate) + 1)
    year(@startDate) + row_number() over(Order By @@spid) - 1
    From t t1, t t2, t t3, t t4
    )
    , dates (LeapYear, StartDate, EndDate, Feb28)
    As (
    Select iif(y % 400 = 0 Or y % 4 = 0 And y % 100 <> 0, 1, 0)
    , iif(@startDate > YearStart, @startDate, YearStart)
    , iif(@endDate < YearEnd, @endDate, YearEnd)
    , Feb28
    From inputYears As y
    Cross Apply (Values (datetimefromparts(y, 2, 28, 0, 0, 0, 0)
    , datetimefromparts(y, 1, 1, 0, 0, 0, 0)
    , datetimefromparts(y, 12, 31, 0, 0, 0, 0))) As dt(Feb28, YearStart, YearEnd)
    )
    Select *
    , Date_Diff = datediff(day, d.StartDate, d.EndDate)
    , Adjusted_Date_Diff = datediff(day, d.StartDate, d.EndDate)
    - iif(d.StartDate <= d.Feb28 And d.EndDate > d.Feb28, d.LeapYear, 0)
    + iif(d.StartDate = d.EndDate, 1, 0)
    From dates As d;

    And if you want - for some reason - to be able to get any date range between 0001 and 9999:

    Declare @startDate date = '0001-01-01'
    , @endDate date = '9999-12-31';

    With t(n)
    As (
    Select t.n
    From (
    Values (0), (0), (0), (0), (0), (0), (0), (0), (0), (0)
    , (0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) As t(n)
    )
    , inputYears (y)
    As (
    Select Top (year(@endDate) - year(@startDate) + 1)
    year(@startDate) + row_number() over(Order By @@spid) - 1
    From t t1, t t2, t t3, t t4
    )
    , dates (LeapYear, StartDate, EndDate, Feb28)
    As (
    Select iif(y % 400 = 0 Or y % 4 = 0 And y % 100 <> 0, 1, 0)
    , iif(@startDate > YearStart, @startDate, YearStart)
    , iif(@endDate < YearEnd, @endDate, YearEnd)
    , Feb28
    From inputYears As y
    Cross Apply (Values (datefromparts(y, 2, 28, 0, 0, 0, 0)
    , datefromparts(y, 1, 1, 0, 0, 0, 0)
    , datefromparts(y, 12, 31, 0, 0, 0, 0))) As dt(Feb28, YearStart, YearEnd)
    )
    Select *
    , Date_Diff = datediff(day, d.StartDate, d.EndDate)
    , Adjusted_Date_Diff = datediff(day, d.StartDate, d.EndDate)
    - iif(d.StartDate <= d.Feb28 And d.EndDate > d.Feb28, d.LeapYear, 0)
    + iif(d.StartDate = d.EndDate, 1, 0)
    From dates As d;

    You will also notice that DATEDIFF only counts 364 days for a normal year - and whether or not that is valid depends on how you define your term dates.  It appears you define the term date as non-inclusive so a term date of 01/01/2021 shows that policy to no longer be in effect as of the start of that date.

    If the term date is defined as inclusive - meaning the policy/contract is effective until the end of the specified date you would need to add one to the calculation for each year so that a normal year would count as 365 and a leap year would count as 366.

    • This reply was modified 4 years, 11 months ago by  Jeffrey Williams. Reason: Removed an extra Top that wasn't needed

    Jeffrey Williams
    “We are all faced with a series of great opportunities brilliantly disguised as impossible situations.”

    ― Charles R. Swindoll

    How to post questions to get better answers faster
    Managing Transaction Logs

  • I like the simplicity of finding the February 29 dates, then subtracting that DesNorton posted.  But it didn't calculate correctly when I used the start date of 01/01/2019 end date of 01/01/2020.  The code still returned the 02/29/2020 date.

    I modified the code to look like this:

    ALTER FUNCTION dbo.GetLeapYearDates(@Date1 AS date, @Date2 AS date)
    RETURNS TABLE WITH SCHEMABINDING
    AS
    RETURN

    --DECLARE @Date1DATE;
    --DECLARE @Date2DATE;

    --SET @Date1 = '2015-01-01';
    --SET @Date2 = '2021-01-01';

    WITH
    T(N) AS (SELECT N FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0),(0),(0),(0),(0),(0),(0)
    , (0),(0),(0),(0)) AS X(N))
    , Nums AS (SELECT TOP(ABS(YEAR(@Date2) - YEAR(@Date1)) +1) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS rownum
    FROM T T1 -- Max = 64
    , T T2 -- Max = 4,096
    , T T3 -- Max = 262,144
    )
    ,
    LeapDays AS
    (
    SELECT DATEADD(DD, 59, DATEADD(YY, (SELECT MIN(YEAR(dt)) FROM (VALUES (@Date1), (@Date2)) AS v(dt)) + rownum -1901, 0)) AS LongFeb
    FROM Nums
    WHERE DAY(DATEADD(DD, 59, DATEADD(YY, (SELECT MIN(YEAR(dt)) FROM (VALUES (@Date1), (@Date2)) AS v(dt)) + rownum -1901, 0))) = 29
    )
    SELECT LongFeb
    FROM LeapDays
    WHERE LongFeb BETWEEN @Date1 AND @Date2
    ;
    GO

    -------------------------------------------------------------
    we travel not to escape life but for life not to escape us
    Don't fear failure, fear regret.

  • below86 wrote:

    I like the simplicity of finding the February 29 dates, then subtracting that DesNorton posted.  But it didn't calculate correctly when I used the start date of 01/01/2019 end date of 01/01/2020.  The code still returned the 02/29/2020 date.

     

    Good catch

  • I think the code below is much simpler.  The code is complete, no function needed.  And, yes, I took a short-cut, so, yes, this code will fail for 2100.  If you want to address that, let me know and we can add the necessary checks for that too.

    ;WITH
    cte_tally10 AS (
    SELECT * FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) AS numbers(number)
    ),
    cte_tally100 AS (
    SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1 AS number
    FROM cte_tally10 c1
    CROSS JOIN cte_tally10 c2
    )
    SELECT Eff_Date, Exp_Date,
    DATEDIFF(DAY, Eff_Date, Exp_Date) - leap_days_count AS Date_diff
    , leap_days_count /*just to show the result, naturally drop it from final code*/

    FROM ( VALUES(1,CAST('20200101' AS date),CAST('20210101' AS date)),
    (2,'20200101','20200701'),
    (3,'20200101','20200228'), (4,'20200101','20200229'),
    (9,'20011114','20190223') /* note: 17 year+ range */
    ) AS test_dates(date_id, Eff_Date, Exp_Date)

    CROSS APPLY (
    SELECT ISNULL(SUM(CASE WHEN year % 4 = 0 THEN 1 ELSE 0 END), 0) AS leap_days_count
    FROM (
    SELECT YEAR(Eff_Date) + t.number AS year
    FROM cte_tally100 t
    WHERE t.number <= YEAR(Exp_date) - YEAR(Eff_Date)
    ) AS all_years
    WHERE DATEADD(DAY, 59, DATEFROMPARTS(year, 1, 1)) BETWEEN Eff_Date AND Exp_Date
    ) AS ca1

    • This reply was modified 4 years, 11 months ago by  ScottPletcher. Reason: Tweaked the code

    SQL DBA,SQL Server MVP(07, 08, 09) "It's a dog-eat-dog world, and I'm wearing Milk-Bone underwear." "Norm", on "Cheers". Also from "Cheers", from "Carla": "You need to know 3 things about Tortelli men: Tortelli men draw women like flies; Tortelli men treat women like flies; Tortelli men's brains are in their flies".

Viewing 12 posts - 1 through 11 (of 11 total)

You must be logged in to reply to this topic. Login to reply