dmoney
dmoney

Reputation: 881

Breaking down a date range in to number of months in each calendar year

I have a large number of date ranges as below, example below. I need to calculate how many months are in each actual calendar year. So this would break down as:


Contract: 123

Start date: 01/11/2016

End date: 01/06/2018


Contract: 456

Start date: 31/05/2017

End date: 01/06/2019


Does anyone know of a solution to handle this? Each contract has a row, all in the same table and the start and end date listed in the same row.

I was originally going down the CTE route but this blew my mind.

Expected outcome:

contract_id    year    number of months
123            2016    2
123            2017    12
123            2018    6
456            2017    6
456            2018    12
456            2019    6

Or similar, I am more than happy to amend my original query to incorporate what the best outcome/method to achieve this is.


Table definition:

Upvotes: 1

Views: 361

Answers (3)

Nițu Alexandru
Nițu Alexandru

Reputation: 714

You can use master..spt_values with type = 'P' to get numbers from 0 to 2047. Filtering this number so it is between year of start date and year of end date and you get the years between the two dates as rows. The EndOfYear and BeginOfYear returns the first date, respectively last date, of each of this years. Months returns the months between the first and last date.

DECLARE @Table TABLE
(
    Contract VARCHAR(5),
    StartDate DATETIME,
    EndDate DATETIME
)

INSERT INTO @Table(Contract, StartDate, EndDate) 
SELECT 'A', '20161101', '20180601' UNION ALL
SELECT 'B', '20170531', '20190601'

SELECT Contract,
    Year = spt_values.number,
    Months = Months.Value
FROM @Table CROSS JOIN
    master..spt_values CROSS APPLY
    (
        SELECT CAST(CONCAT(spt_values.number, '1231') AS DATETIME) AS Value
    ) AS EndOfYear CROSS APPLY
    (
        SELECT DATEADD(YEAR, -1, EndOfYear.Value) + 1 AS Value
    ) AS BeginOfYear CROSS APPLY
    (
        SELECT DATEDIFF(MONTH, IIF(BeginOfYear.Value < StartDate, StartDate, BeginOfYear.Value), IIF(EndOfYear.Value > EndDate, EndDate, EndOfYear.Value)) + 1 AS Value
    ) Months
WHERE type = 'P' AND
    spt_values.number <= YEAR(EndDate) AND
    spt_values.number >= YEAR(StartDate)

Upvotes: 0

George Menoutis
George Menoutis

Reputation: 7260

Here's a possibility:

select t.contract_id,n.id as year,q2.[#months]
from yourtable t
cross apply
(
    select year([Start Date]) as first_year,
    select year([End Date]) as last_year
)q
inner join numbers_table n on n.id between q.first_year and q.last_year
cross apply
(
    select case
        when n.id=first_year then 12-month([Start Date])
        when n.id=last_year then month([End Date])
        else 12
        end as [#months]
)q2

If you don't have a numbers table, put this before the query:

;WITH numbers_table(id) AS
(
  SELECT ROW_NUMBER() OVER (ORDER BY s1.[object_id]) - 1
  FROM sys.all_columns AS s1
  CROSS JOIN sys.all_columns AS s2
)

I would go for the tested variation of Sean Lange, though

Upvotes: 0

Sean Lange
Sean Lange

Reputation: 33581

I would use a tally for this. I keep one on my system as a view which is lightning fast. Here is the view.

create View [dbo].[cteTally] as

WITH
    E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
    E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
    E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
    cteTally(N) AS 
    (
        SELECT  ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
    )
select N from cteTally
GO

Then we need some sample data. Something like this.

declare @Something table
(
    Contract char(1)
    , StartDate date
    , EndDate date
)

insert @Something values
('A', '20161101', '20180601')
, ('B', '20170531', '20190601')

Now we can query against the sample and utilize the tally table to make short work of this.

select s.Contract
    , ContractYear = datepart(year, DATEADD(month, t.N - 1, s.StartDate))
    , NumMonths = count(*)
from @Something s
join cteTally t on t.N <= datediff(month, s.StartDate, s.EndDate) + 1
group by s.Contract
    , datepart(year, DATEADD(month, t.N - 1, s.StartDate))
order by s.Contract
    , datepart(year, DATEADD(month, t.N - 1, s.StartDate))

Upvotes: 1

Related Questions