Home > OS >  SQL - how to get quarters for all dates based on a given current quarter end date, current quarter n
SQL - how to get quarters for all dates based on a given current quarter end date, current quarter n

Time:03-13

I have an issue where I need to determine fiscal quarters, but won't always know the start/end dates for the quarters. They will, however, always be 3 months long. What I will know is the ending date of the current quarter, and what quarter and year that refers to. For example, I might be given:

Current Quarter: Q4
Current Year: 2021
Current Quarter End Date: 1/31/2021

How can I get the quarter for any other date? If any of those 3 values were to change, the query still needs to provide the quarter for any given date based on those 3 parameters.

I came up with the following, which puts the last 4 years into a temp table:

DECLARE @QuarterEnd             DATE            =   '1/31/2022'
,       @CurrentQuarter         INT             =   1
,       @CurrentYear            INT             =   2022
,       @Counter                INT             =   16
,       @qs                     INT             =   0
,       @qe                     INT             =   2
,       @DateToTest             DATE            =   '12/15/2021'

CREATE TABLE #Quarters (
        StartDate   DATE
,       EndDate     DATE
,       Qtr         INT
,       Yr          INT
)

WHILE @Counter <> 0
BEGIN
    INSERT INTO #Quarters VALUES (
            cast(DATEADD(MONTH, DATEDIFF(MONTH, 0, @QuarterEnd)-@qe , 0) as date)
        ,   cast(DATEADD(MONTH, DATEDIFF(MONTH, -1, @QuarterEnd)-@qs, -1) as date)
        ,   @CurrentQuarter
        ,   @CurrentYear
    )
    SET @Counter = @Counter - 1
    SET @qs = @qs   3
    SET @qe = @qe   3
    SET @CurrentQuarter = CASE WHEN @CurrentQuarter = 1 THEN 4 ELSE @CurrentQuarter - 1 END
    SET @CurrentYear = CASE WHEN @CurrentQuarter = 4 THEN @CurrentYear - 1 ELSE @CurrentYear END
END

SELECT  @DateToTest
,       (SELECT CONCAT('Q', Qtr, ' ', Yr) FROM #Quarters WHERE @DateToTest BETWEEN StartDate and EndDate)
FROM    #Quarters

However, this doesn't seem to be practical when I'm running queries that will return hundreds of thousands of records.

I suppose I can throw that into a function and call it with:

SELECT MyQuarter = dbo.MyQuarterFunction(@QuarterEnd, @CurrentQuarter, @CurrentYear, @DateToTest)

There has to be a more efficient way to do this. Any suggestions?

CodePudding user response:

Just create a permanent table called Quarters.

CREATE TABLE dbo.Quarters
(
  StartDate     date,
  QuarterNumber tinyint,
  FiscalYear    int,
  NextQuarterStartDate AS (DATEADD(MONTH, 3, StartDate))
);

INSERT dbo.Quarters(StartDate, QuarterNumber, FiscalYear)
  VALUES('20200201',1,2020),
        ('20200501',2,2020),
        ('20200801',3,2020),
        ('20201101',4,2020),
        ('20210201',1,2021),
        ('20210501',2,2021),
        ('20210801',3,2021),
        ('20211101',4,2021),
        ('20220201',1,2022),
        ('20220501',2,2022),
        ('20220801',3,2022),
        ('20221101',4,2022);

Now any time you are given a date (like GETDATE()) you can find the other information easily:

DECLARE @date date = GETDATE();

SELECT * FROM dbo.Quarters 
  WHERE @date >= StartDate
  AND @date < NextQuarterStartDate;

If you need to support multiple fiscal calendars simultaneously, just add a column (like CalendarID or CompanyID or CustomerID).

And really, you don't even need a calendar or quarters table for this. You already have a table of clients, right? Just add a column to store what month their fiscal year starts. That's really all you need.

CREATE TABLE dbo.Clients
(
  ClientID        int           NOT NULL CONSTRAINT PK_Clients PRIMARY KEY,
  Name            nvarchar(200) NOT NULL CONSTRAINT UQ_ClientName UNIQUE,
  FiscalYearStart tinyint       NOT NULL CONSTRAINT CK_ValidMonth 
    CHECK (FiscalYearStart BETWEEN 1 AND 12)
);

Now let's insert a few rows with some clients with different fiscal years:

INSERT dbo.Clients(ClientID, Name, FiscalYearStart)
  VALUES(1, N'ClientFeb',    2), -- fiscal year starts in February 
        (2, N'ClientMay',    5), -- fiscal year starts in May
        (3, N'ClientNormal', 1); -- fiscal year matches calendar

Now, yes, we need a function, but let's not do any while loops or counters or #temp tables.

CREATE FUNCTION dbo.GetLast16Quarters
(
  @DateToTest date,
  @ClientID   int
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN 
(
  WITH n(n) AS
  (
    SELECT n = 1 UNION ALL 
    SELECT n   1 FROM n WHERE n < 20
  ),
  Last20Quarters(QuarterStart, FiscalYearStart) AS
  (
    SELECT QuarterStart = DATEADD(QUARTER, 1-n, 
      DATEFROMPARTS(YEAR(@DateToTest) 1, FiscalYearStart, 1)), 
      FiscalYearStart
    FROM dbo.Clients CROSS JOIN n WHERE ClientID = @ClientID
  ),
  Last16Quarters AS
  (
    SELECT TOP (16) QuarterStart, 
      y = YEAR(DATEADD(MONTH, 1-FiscalYearStart, QuarterStart)) 
    FROM Last20Quarters WHERE QuarterStart < @DateToTest
    ORDER BY QuarterStart DESC
  )
  SELECT QuarterStart, 
         QuarterEnd = EOMONTH(QuarterStart, 2),
         FiscalYear = y,
         QuarterNumber = ROW_NUMBER() OVER 
           (PARTITION BY y ORDER BY QuarterStart)
  FROM Last16Quarters);

Then to call it:

DECLARE @DateToTest date = '20211215';
SELECT * FROM dbo.GetLast16Quarters(@DateToTest, 1);

Output:

QuarterStart QuarterEnd FiscalYear QuarterNumber
2018-02-01 2018-04-30 2018 1
2018-05-01 2018-07-31 2018 2
2018-08-01 2018-10-31 2018 3
2018-11-01 2019-01-31 2018 4
2019-02-01 2019-04-30 2019 1
2019-05-01 2019-07-31 2019 2
2019-08-01 2019-10-31 2019 3
2019-11-01 2020-01-31 2019 4
2020-02-01 2020-04-30 2020 1
2020-05-01 2020-07-31 2020 2
2020-08-01 2020-10-31 2020 3
2020-11-01 2021-01-31 2020 4
2021-02-01 2021-04-30 2021 1
2021-05-01 2021-07-31 2021 2
2021-08-01 2021-10-31 2021 3
2021-11-01 2022-01-31 2021 4

CodePudding user response:

Assuming that you have two input variables:

declare @quarter_end date = '2021-01-31';
declare @current_quarter int = 4;

You can calculate the first month of financial year:

declare @first_month_of_fy int = (month(@quarter_end) - @current_quarter * 3   12) % 12   1;
-- 2 i.e. February

And use that value to calculate the quarter and year for any date using some math:

select *
from (values
    ('2020-12-15'),
    ('2021-01-15'),
    ('2021-12-15'),
    ('2022-01-15')
) as t(testdate)
cross apply (select
    (month(testdate) - @first_month_of_fy   12) % 12   1
) as ca1(month_of_fy)
cross apply (select
    (month_of_fy - 1) / 3   1,
    year(dateadd(month, 12 - month_of_fy, dateadd(day, - day(testdate)   1, testdate)))
) as ca2(fy_quarter, fy_year)

DB<>Fiddle

  • Related