Home > database >  Calculate quarter for dates given an example end date and quarter number
Calculate quarter for dates given an example end date and quarter number

Time:03-17

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

CodePudding user response:

I ended up creating a function to handle this. Since I'm given the last day of the quarter, which quarter it is, and which year it is, I can determine the start and end date of that fiscal year. Since a quarter is always 3 months, I can also determine which months fall into which quarter.

The first 4 variables, @qa, @qb, @qc, @qd hold a comma separated list of the months within each quarter (@qa is current quarter, @qb is current quarter -1, @qc is current quarter -2, and @qd is current quarter -3)

The second 2 variables determine the first day and last day of the fiscal calendar

To get the quarter & year, I first get the month from the supplied date (@Date) and see if it's in @qa, @qb, @qc or @qd. That tells me the fiscal quarter.

Finally, I compare the given date to the start and end date of the current fiscal year, and to the 6 years prior (going back 6 years is enough for what I need)

CREATE FUNCTION [dbo].[FunctionNameHere]
(
    @Date           DATE
,   @QuarterEnd     DATE
,   @CurrentQuarter INT
,   @CurrentYear    INT
)
RETURNS VARCHAR(7)
AS
BEGIN

DECLARE     @qa                 VARCHAR(8)  = (concat(datepart(m, dateadd(m,  0, @QuarterEnd)),',', datepart(m, dateadd(m, -1,  @QuarterEnd)),',', datepart(m, dateadd(m, -2,  @QuarterEnd))))
DECLARE     @qb                 VARCHAR(8)  = (concat(datepart(m, dateadd(m, -3, @QuarterEnd)),',', datepart(m, dateadd(m, -4,  @QuarterEnd)),',', datepart(m, dateadd(m, -5,  @QuarterEnd))))
DECLARE     @qc                 VARCHAR(8)  = (concat(datepart(m, dateadd(m, -6, @QuarterEnd)),',', datepart(m, dateadd(m, -7,  @QuarterEnd)),',', datepart(m, dateadd(m, -8,  @QuarterEnd))))
DECLARE     @qd                 VARCHAR(8)  = (concat(datepart(m, dateadd(m, -9, @QuarterEnd)),',', datepart(m, dateadd(m, -10, @QuarterEnd)),',', datepart(m, dateadd(m, -11, @QuarterEnd))))
DECLARE     @YearStart          DATE        = DATEADD(d, 1, DATEADD(q, -@CurrentQuarter, @QuarterEnd))
DECLARE     @YearEnd            DATE        = DATEADD(q, 4-@CurrentQuarter, @QuarterEnd)

DECLARE     @Qtr                VARCHAR(8)  =   CONCAT('Q', CASE WHEN DATEPART(m, @Date) IN (SELECT value FROM string_split(@qa, ',')) THEN @CurrentQuarter 

                                                                 WHEN DATEPART(m, @Date) IN (SELECT value FROM string_split(@qb, ',')) THEN CASE WHEN @CurrentQuarter = 1 THEN 4
                                                                                                                                                 WHEN @CurrentQuarter = 2 THEN 1
                                                                                                                                                 WHEN @CurrentQuarter = 3 THEN 2
                                                                                                                                                 WHEN @CurrentQuarter = 4 THEN 3 END

                                                                 WHEN DATEPART(m, @Date) IN (SELECT value FROM string_split(@qc, ',')) THEN CASE WHEN @CurrentQuarter = 1 THEN 3
                                                                                                                                                 WHEN @CurrentQuarter = 2 THEN 4
                                                                                                                                                 WHEN @CurrentQuarter = 3 THEN 1
                                                                                                                                                 WHEN @CurrentQuarter = 4 THEN 2 END

                                                                 WHEN DATEPART(m, @Date) IN (SELECT value FROM string_split(@qd, ',')) THEN CASE WHEN @CurrentQuarter = 1 THEN 2
                                                                                                                                                 WHEN @CurrentQuarter = 2 THEN 3
                                                                                                                                                 WHEN @CurrentQuarter = 3 THEN 4
                                                                                                                                                 WHEN @CurrentQuarter = 4 THEN 1 END
                                                END,
                                                ' ',
                                                CASE WHEN @Date BETWEEN @YearStart AND @YearEnd THEN @CurrentYear
                                                     WHEN @Date BETWEEN dateadd(Year, -1, @YearStart) AND dateadd(Year, -1, @YearEnd) THEN @CurrentYear - 1
                                                     WHEN @Date BETWEEN dateadd(Year, -2, @YearStart) AND dateadd(Year, -2, @YearEnd) THEN @CurrentYear - 2
                                                     WHEN @Date BETWEEN dateadd(Year, -3, @YearStart) AND dateadd(Year, -3, @YearEnd) THEN @CurrentYear - 3
                                                     WHEN @Date BETWEEN dateadd(Year, -4, @YearStart) AND dateadd(Year, -4, @YearEnd) THEN @CurrentYear - 4
                                                     WHEN @Date BETWEEN dateadd(Year, -5, @YearStart) AND dateadd(Year, -5, @YearEnd) THEN @CurrentYear - 5
                                                     WHEN @Date BETWEEN dateadd(Year, -6, @YearStart) AND dateadd(Year, -6, @YearEnd) THEN @CurrentYear - 6
                                                     ELSE 9999 END)

    return @Qtr

END
  • Related