Add number of working days to Date in snowflake - snowflake-cloud-data-platform

I'm trying to add no. of business days to a date field.
Below is my logic, this is not working if day falls on Monday and if no. of days are more than 5.
dateadd(DAY,
(iff(dayofweek(to_date(Start_Date_Column) ) = 1, 0 ,
(TRUNCATE(((dayofweek(to_date(Start_Date_Column)) + No_DAYS - 1)/5)) * 2)) + No_DAYS) , to_date(Start_Date_Column));
e.g.. For the below scenario the date is moving to 2021-01-09(which is Saturday) instead of monday(2021-01-11)
select
dateadd(DAY,
(iff(dayofweek(to_date('2021-01-04') ) = 1, 0 ,
(TRUNCATE(((dayofweek(to_date('2021-01-04')) + 5 - 1)/5)) * 2)) + 5) , to_date('2021-01-04'))

Recursive CTE to get next business day, after adding specific number of business days to a date.
WITH RECURSIVE
biz_day (rown, dt) AS
(
SELECT 1,'2022-05-07'::date
union all
SELECT
case when dayname(dateadd(day,1,dt)) not in ('Sat','Sun')
then rown+1
else rown end ,
dateadd(day,1,dt)
from biz_day where rown <= $No_DAYS
)
SELECT min(dt) orig_dt,max(dt) nxt_biz_date FROM biz_day;
ORIG_DT
NXT_BIZ_DATE
2022-05-07
2022-05-20
Original table -
Column next_dy to be modified with next business day, based on specific number of days (e.g. 10)
select * from dates;
DY
NEXT_DY
2022-05-05
NULL
2022-05-09
NULL
2022-05-11
NULL
2022-05-14
NULL
2022-05-15
NULL
2022-05-17
NULL
2022-05-27
NULL
(UDF) Function creation including recursive CTE for getting next biz day -
CREATE OR REPLACE FUNCTION next_b_date(dt date,no_day number)
RETURNS date
LANGUAGE SQL
AS
$$
WITH RECURSIVE
biz_day (rown, cte_dt) AS
(
SELECT 1,dt
union all
SELECT
case when dayname(dateadd(day,1,cte_dt)) not in ('Sat','Sun')
then rown+1
else rown end ,
dateadd(day,1,cte_dt)
from biz_day where rown <= no_day
)
SELECT max(cte_dt) nxt_biz_date FROM biz_day
$$;
Anonymous Block to call function and update source table -
EXECUTE IMMEDIATE $$
DECLARE
p_dt date;
n_dt date;
no_days number;
c1 CURSOR FOR SELECT dy FROM dates;
BEGIN
for record in c1 do
p_dt:=record.dy;
no_days:=10;
n_dt:=(select next_b_date(:p_dt,:no_days));
update dates set next_dy = :n_dt where dy=:p_dt;
end for;
RETURN 0;
END;
$$
;
Table after running the update -
select * from dates;
DY
NEXT_DY
2022-05-05
2022-05-19
2022-05-09
2022-05-23
2022-05-11
2022-05-25
2022-05-14
2022-05-27
2022-05-15
2022-05-27
2022-05-17
2022-05-31
2022-05-27
2022-06-10
Below can also be used, which has little-bit of hard-coding
i.e. multiply number of days with 3 to go bit over extra in days
(to compensate for intermediate sat/sun) for fetching business days.
set no_days=11;
update dates dt1 set next_dy = dt2.n_dy from
(
with cte1 as
(select row_number() over (partition by dy order by seq4()) rn,dy,
dateadd(day,rn,dy) day1 from dates,
table(generator(rowcount=>$no_days*3))), cte2 as
(select row_number() over (partition by dy order by seq4()) rn,dy,day1
from cte1 where dayname(day1)
not in ('Sat','Sun') )
select dy,max(day1) n_dy from cte2 where rn<=$no_days
group by dy order by dy
) dt2
where dt1.dy = dt2.dy;
Table after update -
select * from dates;
DY
NEXT_DY
2022-05-05
2022-05-20
2022-05-09
2022-05-24
2022-05-11
2022-05-26
2022-05-14
2022-05-30
2022-05-15
2022-05-30
2022-05-17
2022-06-01
2022-05-27
2022-06-13
Old - solution (not valid) to add days and move to next Monday -
To get next business day, check if day after adding is sat or sun and if so, get to next Monday using function next_day
Query with some test-data:
with date_cte(bizday) as
(select * from values
('2022-05-05'::date),
('2022-05-11'::date),
('2022-05-14'::date),
('2022-05-15'::date),
('2022-05-21'::date),
('2022-05-27'::date)
)
select bizday orig_date,dayname(bizday) orig_day,
$No_DAYS No_days,
dateadd(day,$No_DAYS,bizday) next_day,
case when dayname(dateadd(day,$No_DAYS,bizday)) = 'Sat'
then next_day(dateadd(day,$No_DAYS,bizday),'Monday')
when dayname(dateadd(day,$No_DAYS,bizday)) = 'Sun'
then next_day(dateadd(day,$No_DAYS,bizday),'Monday')
else dateadd(day,$No_DAYS,bizday) end next_b_day,
dayname(next_b_day) next_b_dayname
from date_cte;
ORIG_DATE
ORIG_DAY
NO_DAYS
NEXT_DAY
NEXT_B_DAY
NEXT_B_DAYNAME
2022-05-05
Thu
10
2022-05-15
2022-05-16
Mon
2022-05-11
Wed
10
2022-05-21
2022-05-23
Mon
2022-05-14
Sat
10
2022-05-24
2022-05-24
Tue
2022-05-15
Sun
10
2022-05-25
2022-05-25
Wed
2022-05-21
Sat
10
2022-05-31
2022-05-31
Tue
2022-05-27
Fri
10
2022-06-06
2022-06-06
Mon
Refer for date-time
Also, you might need to add more here as the definition of a business day can be varied.

Related

Alternative for Sys_Calendar of teradata in snowflake

Do we have any alternative for teradata Sys_Calendar.CALENDAR function in snowflake? I couldnt find any table or builtin functions to achive this
[The Sys_Calendar.CALENDAR system view helps to extend the properties of a DATE data type column by means of a join. The columns of the view contain data only for the active calendar for the session.
The calendar dates range from 1900 to 2100 and are stored in a table in the Sys_Calendar database.]
BASIC -> BELOW
ADVANCED -> AUTOMATICALLY PULLS IN HOLIDAYS AND PRETTY EMOJI FLAGS FOR YOUR COUNTRY BASED ON CURRENT_IP()
WITH GAPLESS_ROW_NUMBERS AS (
SELECT ROW_NUMBER() OVER (ORDER BY seq4()) - 1 as "ROW_NUMBER"
FROM TABLE(GENERATOR(rowcount => 366 * (2100 - 1970)) )
-- rowcount is 366 days x (2100 - 1970) years to cover leap years. A later filter can remove the spillover days
)
SELECT
DATEADD('DAY', ROW_NUMBER, DATE(0))::DATE as DATE -- Dimension starts on 1970-01-01 but a different value can be entered if desired - replace DATE(0) with '1900-01-01' to start at 1900 for example
, EXTRACT(year FROM DATE) as YEAR
, EXTRACT(month FROM DATE) as MONTH
, EXTRACT(day FROM DATE) as DAY
, EXTRACT(dayofweek FROM DATE) as DAY_OF_WEEK
, EXTRACT(dayofyear FROM DATE) as DAY_OF_YEAR
, EXTRACT(quarter FROM "DATE") as QUARTER
, MIN("DAY_OF_YEAR") OVER (PARTITION BY "YEAR", "QUARTER") as "QUARTER_START_DAY_OF_YEAR"
, "DAY_OF_YEAR" - "QUARTER_START_DAY_OF_YEAR" + 1 as "DAY_OF_QUARTER"
, TO_VARCHAR("DATE", 'MMMM') as "MONTH_NAME"
, TO_VARCHAR("DATE", 'MON') as "MONTH_NAME_SHORT"
, CASE "DAY_OF_WEEK"
WHEN 0 THEN 'Sunday'
WHEN 1 THEN 'Monday'
WHEN 2 THEN 'Tuesday'
WHEN 3 THEN 'Wednesday'
WHEN 4 THEN 'Thursday'
WHEN 5 THEN 'Friday'
WHEN 6 THEN 'Saturday'
END as "DAY_NAME"
, CASE "DAY_OF_WEEK"
WHEN 0 THEN TRUE
WHEN 6 THEN TRUE
ELSE FALSE END as "IS_WEEKEND"
, TO_VARCHAR("DATE", 'DY') as "DAY_NAME_SHORT"
, EXTRACT(yearofweekiso FROM "DATE") as "ISO_YEAR"
, EXTRACT(weekiso FROM "DATE") as "ISO_WEEK"
, CASE
WHEN "ISO_WEEK" <= 13 THEN 1
WHEN "ISO_WEEK" <= 26 THEN 2
WHEN "ISO_WEEK" <= 39 THEN 3
ELSE 4
END as "ISO_QUARTER"
, EXTRACT(dayofweekiso FROM "DATE") as "ISO_DAY_OF_WEEK"
, MAX("DAY_OF_YEAR") OVER (PARTITION BY "YEAR") as "DAYS_IN_YEAR"
, "DAYS_IN_YEAR" - "DAY_OF_YEAR" as "DAYS_REMAINING_IN_YEAR"
FROM
GAPLESS_ROW_NUMBERS
WHERE "YEAR" BETWEEN 1950 AND 2050
GROUP BY DAY_OF_YEAR,YEAR ,QUARTER ,GAPLESS_ROW_NUMBERS.ROW_NUMBER
ORDER BY 1,2,3,4

SQL - Group By Week to begin on a specific weekday without involving two transactions?

I am writing a query that returns the sum of rows for the last 10 weeks FRI-THURS.
It uses a group by to show the sum of each week:
WITH Vars (Friday) -- get current week Fridays Date
AS (
SELECT CAST(DATEADD(DAY,(13 - (##DATEFIRST + DATEPART(WEEKDAY,GETDATE())))%7,GETDATE()) AS DATE) As 'Friday'
)
SELECT datepart(week, DateField) AS WeekNum, COUNT(*) AS Counts
FROM Table
WHERE DateField >= DATEADD(week,-9, (SELECT Friday from Vars))
GROUP BY datepart(week, DateField)
ORDER BY WeekNum DESC
The problem is every week starts on Monday so the Group By doesn't group the dates on how I want it. I want a week to be defined as FRI-THURS.
One workaround to this is to use DATEFIRST. e.g:
SET DATEFIRST = 5; --set beginning of each week to Friday
WITH Vars (Friday) -- get current week Fridays Date
... rest of query
However due to limitations on the interface I am writing this query I cannot have two separate statements run. It needs to be one query with no semicolons.
How can I achieve this?
This should do it. First pre-compute once the StartingFriday of 9 weeks ago, rather than doing that for each row. Then compute the dfYear and dfWeek giving them alias-es, where their DateField is after the starting friday. Lastly, Count/GroupBy/OrderBy.
Declare #StartingFriday as date =
DATEADD(week,-9, (DATEADD(day, - ((Datepart(WEEKDAY,GETDATE()) +1) % 7) , GETDATE())) ) ;
SELECT dfYear, dfWeek, COUNT(*) AS Counts
FROM
(Select -- compute these here, and use alias in Select, GroupBy, OrderBy
(Datepart(Year,(DATEADD(day, - ((Datepart(WEEKDAY,DateField) +1) % 7) , DateField)) ) )as dfYear
,(Datepart(Week,(DATEADD(day, - ((Datepart(WEEKDAY,DateField) +1) % 7) , DateField)) ) )as dfWeek
From Table
WHERE #StartingFriday <= DateField
) as aa
group by dfYear, dfWeek
order by dfYear desc, dfWeek desc
-- we want the weeknum of the (Friday on or before the DateField)
-- the % (percent sign) is the math MODULO operator.
-- used to get back to the nearest Friday,
-- day= Fri Sat Sun Mon Tue Wed Thu
-- weekday= 6 7 1 2 3 4 5
-- plus 1 = 7 8 2 3 4 5 6
-- Modulo7= 0 1 2 3 4 5 6
-- which are the days to subtract from DateField
-- to get to its Friday start of its week.
I did some testing with this
declare #dt as date = '8/17/18';
select ((DATEPART(WEEKDAY,#dt) +1) % 7) as wd
,(DATEADD(day, - ((Datepart(WEEKDAY,#dt) +1) % 7) , #dt)) as Fri
,(Datepart(Week,(DATEADD(day, - ((Datepart(WEEKDAY,#dt) +1) % 7) , #dt)) ) )as wk
,DATEADD(week,-9, (DATEADD(day, - ((Datepart(WEEKDAY,#dt) +1) % 7) , #dt)) ) as StartingFriday

Identify All Holiday Dates By Year using TSQL

I did a lot of searching for an easy solution to dynamically identify U.S. federal holidays by year. I wasn't able to find much information for the trickier holidays. Holidays like New Year's Day or Independence Day are easy to program as they are static. However, some are more difficult to identify programmatically such as Presidents' Day (3rd Monday in February) or Thanksgiving (4th Thursday in November).
I know this is an old question, but we have a sweet Scalar-valued Function that returns 1 if it's a holiday and 0 if it is not. We do have to manually add Observed Dates - We use the https://www.timeanddate.com/holidays/us/ to get the observed dates.
First, create the function:
USE [DATABASE]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[IsHoliday](#date as DATETIME)
RETURNS bit
AS
BEGIN
if #date is not null
begin
-- JAN
IF Month(#date)=1 AND day(#date)=1 return 1 -- New Years Day
IF Month(#date)=1 AND DATEPART(weekday, #date)=2 and day(#date)>14 and day(#date)<=21 return 1 -- Martin Luther King, Jr. Day
-- FEB
IF Month(#date)=2 AND DATEPART(weekday, #date)=1 and day(#date)<=7 return 1 -- Super Bowl Sunday
IF Month(#date)=2 AND day(#date)=14 return 1 -- Valentine's Day
IF Month(#date)=2 AND DATEPART(weekday, #date)=2 and day(#date)>14 and day(#date)<=21 return 1 -- Presidents' Day
-- MAR
IF Month(#date)=3 AND day(#date)=17 return 1 -- St Patrick's Day
-- MAY
IF Month(#date)=5 AND DATEPART(weekday, #date)=1 and day(#date)>7 and day(#date)<=14 return 1 -- Mother's day
IF Month(#date)=5 AND DATEPART(weekday, #date)=2 and day(#date)>24 and day(#date)<=31 return 1 --Memorial Day
-- JUN
IF Month(#date)=6 AND day(#date)=19 return 1 -- Juneteenth
IF Month(#date)=6 AND DATEPART(weekday, #date)=2 and day(#date)>14 and day(#date)<=21 return 1 --Father's Day
-- JUL
IF Month(#date)=7 AND day(#date)=4 return 1 -- July 4th
-- SEP
IF Month(#date)=9 AND DATEPART(weekday, #date)=2 and day(#date)<=7 return 1--Labor Day
-- OCT
IF Month(#date)=10 AND DATEPART(weekday, #date)=2 and day(#date)>7 and day(#date)<=14 return 1 --Columbus Day
IF Month(#date)=10 AND day(#date)=31 return 1 -- Halloween
-- NOV
IF Month(#date)=11 AND day(#date)=11 return 1 -- Veteran's Day
IF Month(#date)=11 AND DATEPART(weekday, #date)=5 and day(#date)>21 and day(#date)<=28 return 1 --Thanksgiving
-- DEC
IF Month(#date)=12 AND day(#date)=24 return 1 -- Christmas Eve
IF Month(#date)=12 AND day(#date)=25 return 1 -- Christmas Day
IF Month(#date)=12 AND day(#date)=31 return 1 -- NYE
-- Observed Dates
if month(#date)=1 AND day(#date)=2 AND year(#date)=2017 return 1 -- New Years Day observed for 2017
if month(#date)=11 AND day(#date)=10 AND year(#date)=2017 return 1 -- Veteran's Day observed for 2017
if month(#date)=11 AND day(#date)=12 AND year(#date)=2018 return 1 -- Veteran's Day observed for 2018
if month(#date)=7 AND day(#date)=3 AND year(#date)=2020 return 1 -- 4th of July Observed for 2021
if month(#date)=6 AND day(#date)=18 AND year(#date)=2021 return 1 -- Juneteenth observed for 2021
if month(#date)=7 AND day(#date)=5 AND year(#date)=2021 return 1 -- 4th of July Observed for 2021
if month(#date)=6 AND day(#date)=20 AND year(#date)=2022 return 1 -- Juneteenth observed for 2022
if month(#date)=12 AND day(#date)=26 AND year(#date)=2022 return 1 -- Christmas Day observed for 2022
if month(#date)=1 AND day(#date)=2 AND year(#date)=2023 return 1 -- New Years Day observed for 2023
if month(#date)=11 AND day(#date)=10 AND year(#date)=2023 return 1 -- Veteran's Day observed for 2023
if month(#date)=7 AND day(#date)=3 AND year(#date)=2026 return 1 -- 4th of July Observed for 2026
if month(#date)=6 AND day(#date)=18 AND year(#date)=2027 return 1 -- Juneteenth observed for 2027
if month(#date)=7 AND day(#date)=5 AND year(#date)=2027 return 1 -- 4th of July Observed for 2027
if month(#date)=11 AND day(#date)=10 AND year(#date)=2028 return 1 -- Veteran's Day observed for 2028
if month(#date)=11 AND day(#date)=12 AND year(#date)=2029 return 1 -- Veteran's Day observed for 2029
end
return 0
END
GO
Then call the function:
SELECT dbo.[IsHoliday](GETDATE())
SELECT dbo.[IsHoliday]('2021-07-05 09:20:51.270')
Here is the solution I came up with.
I created a table variable to store the entire years dates:
DECLARE #DateTable TABLE
(
dtDate DATE,
dtMonth VARCHAR(10),
dtDayName VARCHAR(10),
dtDayRank INT
);
Populated first 3 columns of the #DateTable:
DECLARE #Year CHAR(4), #CurrentDate DATE
SET #Year = '2018'
SET #CurrentDate = CAST(#Year + '0101' AS DATE)
WHILE #CurrentDate <= CAST(#Year + '1231' AS DATE)
BEGIN
INSERT INTO #DateTable (dtDate, dtMonth, dtDayName)
VALUES (#CurrentDate, DATENAME(mm, #CurrentDate), DATENAME(dw, #CurrentDate))
SET #CurrentDate = DATEADD(dd, 1, #CurrentDate)
END;
Once I had the table populated, I ranked the rows and updated the table:
UPDATE #DateTable
SET dtDayRank = rankdates.DayRank
FROM #DateTable datatable
INNER JOIN (
SELECT
dtDate,
DayRank = RANK() OVER (PARTITION BY dtMonth, dtDayName ORDER BY dtDate) -- rank each DayOfWeek in order
FROM #DateTable
) rankdates ON datatable.dtDate = rankdates.dtDate;
Sample Output from #DateTable
Once I had the #DateTable populated, I could use logic to identify specific days.
SELECT
HolidayName = 'Presidents'' Day',
ObservedDayOfWeek = dtDayName,
HolidayObservedDate = dtDate
FROM #DateTable
WHERE dtMonth = 'February'
AND dtDayName = 'Monday'
AND dtDayRank = 3
SELECT
HolidayName = 'Thanksgiving Day',
ObservedDayOfWeek = dtDayName,
HolidayObservedDate = dtDate
FROM #DateTable
WHERE dtMonth = 'November'
AND dtDayName = 'Thursday'
AND dtDayRank = 4
Output
I liked how this solution worked out because I can identify any date in the year by using a predicate equal to the month, day of the week and how many times this day of the week has occurred in this month. I made this into a stored procedure and table valued function so that I can run it by passing a year and it returns all holidays for that year.
Is this a good solution...is there an easier way?
There's some math you can use to make your SQL life easier e.g. 3rd Monday in February mathematically has to be between the 15th and the 21st (the earliest 3rd Monday has 14 days before it; the latest 3rd Monday can have no more than 20 days before it). If you have a tally table, it will be pretty easy to find all the dates. Here's how you can do it for president's day
with t1 as
(SELECT 1 num
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1)) subTable(n)
),
TallyTable as
(
SELECT TOP 10000 ROW_NUMBER() OVER (ORDER BY (SELECT 1)) n
FROM t1 a
CROSS JOIN t1 b
CROSS JOIN t1 c
CROSS JOIN t1 d
CROSS JOIN t1 e
CROSS JOIN t1 f
),
DateTable as
(
SELECT DateAdd(day,n,'1/1/2018') DateValue
FROM TallyTable
)
SELECT *
FROM DateTable DT
WHERE DatePart(month,DT.DateValue) = 2 --February
AND DatePart(dw,DT.DateValue) = 2 --Monday
AND DatePart(day,DT.DateValue) BETWEEN 15 AND 21; --Day is between 15 and 21

Determine number of Days in month returns different count for Month of March

Here is a weird one for you all.
I need to determine the number of days in a Month
;WITH cteNetProfit AS
(
---- NET PROFIT
SELECT DT.CreateDate
, SUM(DT.Revenue) as Revenue
, SUM(DT.Cost) as Cost
, SUM(DT.GROSSPROFIT) AS GROSSPROFIT
FROM
(
SELECT CAST([createDTG] AS DATE) as CreateDate
, SUM(Revenue) as Revenue
, SUM(Cost) as Cost
, SUM(REVENUE - COST) AS GROSSPROFIT
FROM [dbo].[CostRevenueSpecific]
WHERE CAST([createDTG] AS DATE) > CAST(GETDATE() - 91 AS DATE)
AND CAST([createDTG] AS DATE) <= CAST(GETDATE() - 1 AS DATE)
GROUP BY createDTG
UNION ALL
SELECT CAST([CallDate] AS DATE) AS CreateDate
, SUM(Revenue) as Revenue
, SUM(Cost) as Cost
, SUM(REVENUE - COST) AS GROSSPROFIT
FROM abc.PublisherCallByDay
WHERE CAST([CallDate] AS DATE) > CAST(GETDATE() - 91 AS DATE)
AND CAST([CallDate] AS DATE) <= CAST(GETDATE() - 1 AS DATE)
GROUP BY CALLDATE
) DT
GROUP BY DT.CreateDate
)
select distinct MONTH(CREATEDATE), DateDiff(Day,CreateDate,DateAdd(month,1,CreateDate))
FROM cteNetProfit
For some reason it is returning two different results for the month of March 2016 one result is 30 and the other 31(which of course is correct) I validate that the underlying data only has 31 days worth of data for the Month of March. Since Feb is a leap year can this affect the DATEDIFF function. The remaining months return the correct #.
2 29
3 31
3 30
4 30
5 31
Thanks for the input, however, I found the solution elsewhere
select Distinct MONTH(CREATEDATE), Day(EOMONTH(CreateDate))
FROM cteNetProfit
The difference comes when you hit the 2016-03-31 date. If you run the query below for 2016-03-30 and 2016-03-31, the results of adding 1 MONTH using DATEADD, in both instances, is 2016-04-30. It returns the last day of the next month.
SELECT DATEADD(MONTH,1,'2016-03-30') , DATEADD(MONTH,1,'2016-03-31')
This syntax seemed to work (courtesy of https://raresql.com/2013/01/06/sql-server-get-number-of-days-in-month/).
SELECT DAY(DATEADD(ms,-2,DATEADD(MONTH, DATEDIFF(MONTH,0,#DATE)+1,0))) AS [Current Month]

Apply week number to dates for whole weeks only

The time is: (m/d/yyyy) => 2009/01/04
Using this command using datepart(wk,'20090104') I can get the week number (for any given date).
So :
SELECT datepart(wk,'20090101') //1
SELECT datepart(wk,'20090102') //1
SELECT datepart(wk,'20090103') //1
SELECT datepart(wk,'20090104') //2
So far so good.
The problem :
Those 3 first dates are not part of a full week, so I can't put them in a fixed 52-week chart.
Our company needs to see information about each whole week in the 52 weeks of a year. (Each year has 52 whole weeks).
So 20090101 doesn't belong to the first week of 2009 !
It belongs to the previous year (which is irrelevant to my question)
So I need a UDF (I've been searching a lot, and ISOWEEK is not answering my needs) which by a given datetime, will give me the Week Number (week = whole week, so partial weeks aren't considered).
Example :
calcweekNum ('20090101') //52 ...from the last year
calcweekNum ('20090102') //52 ...from the last year
calcweekNum ('20090103') //52 ...from the last year
calcweekNum ('20090104') //1
..
..
calcweekNum ('20090110') //1
calcweekNum ('20090111') //2
calcweekNum ('20090112') //2
...
Here's a different approach. All you need to supply is the year:
DECLARE #year INT = 2009;
DECLARE #start SMALLDATETIME;
SET #start = DATEADD(YEAR, #year-1900, 0);
;WITH n AS
(
SELECT TOP (366) -- in case of leap year
d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY name)-1, #start)
FROM sys.all_objects
),
x AS
(
SELECT md = MIN(d) FROM n
WHERE DATEPART(WEEKDAY, d) = 1 -- assuming DATEFIRST is Sunday
),
y(d,wk) AS
(
SELECT n.d, ((DATEPART(DAYOFYEAR, n.d) - DATEDIFF(DAY, #start, x.md)-1)/7) + 1
FROM n CROSS JOIN x
WHERE n.d >= x.md
AND n.d < DATEADD(YEAR, 1, #start)
)
SELECT [date] = d, [week] = wk
FROM y WHERE wk < 53
ORDER BY [date];
Results:
date week
---------- ----
2009-01-04 1
2009-01-05 1
2009-01-06 1
2009-01-07 1
2009-01-08 1
2009-01-09 1
2009-01-10 1
2009-01-11 2
2009-01-12 2
...
2009-12-25 51
2009-12-26 51
2009-12-27 52
2009-12-28 52
2009-12-29 52
2009-12-30 52
2009-12-31 52
Note that week 52 won't necessarily be a full week, and that in some cases (e.g. 2012), the last day or two of the year might fall in week 53, so they're excluded.
An alternative approach is to repeat the MIN expression twice:
DECLARE #year INT = 2009;
DECLARE #start SMALLDATETIME;
SET #start = DATEADD(YEAR, #year-1900, 0);
;WITH n AS
(
SELECT TOP (366) -- in case of leap year
d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY name)-1, #start)
FROM sys.all_objects
),
y(d,wk) AS
(
SELECT n.d, ((DATEPART(DAYOFYEAR, n.d) - DATEDIFF(DAY, #start, (SELECT MIN(d)
FROM n WHERE DATEPART(WEEKDAY, d) = 1))-1)/7) + 1
FROM n
WHERE n.d >= (SELECT md = MIN(d) FROM n WHERE DATEPART(WEEKDAY, d) = 1)
AND n.d < DATEADD(YEAR, 1, #start)
)
SELECT [date] = d, [week] = wk
FROM y WHERE wk < 53
ORDER BY d;
Here's a function for you to calculate it on the fly:
CREATE FUNCTION dbo.WholeWeekFromDate (
#Date datetime
)
RETURNS tinyint
AS BEGIN
RETURN (
SELECT DateDiff(Day, DateAdd(Year, DateDiff(Year, 0, CalcDate), 0), CalcDate) / 7 + 1
FROM (SELECT DateAdd(Day, (DateDiff(Day, 0, #Date) + 1) / 7 * 7, 0)) X (CalcDate)
);
END;
I don't recommend you use it, as it may perform badly due to being called once for every row. If you absolutely must have a function to use in real queries, then convert it to an inline function returning a single column and row, and use it as so:
SELECT
OtherColumns,
(SELECT WeekNumber FROM dbo.WholeWeekFromDate(DateColumn)) WeekNumber
FROM
YourTable;
This will allow it to be "inlined" in the execution plan and perform significantly better.
But even better, as others have suggested, is to use a BusinessDate table. Here's a head start on creating one for you:
CREATE TABLE dbo.BusinessDate (
BusinessDate date NOT NULL CONSTRAINT PK_BusinessDate PRIMARY KEY CLUSTERED,
WholeWeekYear smallint NOT NULL
CONSTRAINT CK_BusinessDate_WholeWeekYear_Valid
CHECK (WholeWeekYear BETWEEN 1900 AND 9999),
WholeWeekNumber tinyint NOT NULL
CONSTRAINT CK_BusinessDate_WholeWeekNumber_Valid
CHECK (WholeWeekNumber BETWEEN 1 AND 53),
Holiday bit CONSTRAINT DF_BusinessDate_Holiday DEFAULT (0),
Weekend bit CONSTRAINT DF_BusinessDate_Weekend DEFAULT (0),
BusinessDay AS
(Convert(bit, CASE WHEN Holiday = 0 AND Weekend = 0 THEN 1 ELSE 0 END)) PERSISTED
);
And I'll even populate it from 1900-01-01 through 2617-09-22 (Is that enough for the projected life of your product? And it's only 7.8MB so don't fret over size):
WITH A (N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
B (N) AS (SELECT 1 FROM A F, A A, A L, A C, A O, A N),
C (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM B),
Dates AS (
SELECT
N,
DateAdd(Day, N, '18991231') Dte,
DateAdd(Day, N / 7 * 7, '19000101') CalcDate
FROM C
)
INSERT dbo.BusinessDate
SELECT
Dte,
Year(CalcDate),
DateDiff(Day, DateAdd(Year, DateDiff(Year, 0, CalcDate), 0), CalcDate) / 7 + 1,
0,
(N + 6) % 7 / 5 -- calculate weekends
FROM Dates; -- 3-7 seconds or so on my VM server
Then join to the table on the date, and use the WholeWeekNumber column for your output. You might also consider adding a WeekNumberYear because it's going to be a tad difficult to figure out that the 52 of 2009-01-01 really belongs to 2008 without this... a strange data point in there for sure if you don't (laugh).
Example table contents:
BusinessDate WholeWeekYear WholeWeekNumber Holiday Weekend BusinessDay
------------ ------------- --------------- ------- ------- -----------
1/1/2009 2008 52 0 0 1
1/2/2009 2008 52 0 0 1
1/3/2009 2008 52 0 1 0
1/4/2009 2009 1 0 1 0
1/5/2009 2009 1 0 0 1
1/6/2009 2009 1 0 0 1
1/7/2009 2009 1 0 0 1
1/8/2009 2009 1 0 0 1
1/9/2009 2009 1 0 0 1
1/10/2009 2009 1 0 1 0
1/11/2009 2009 2 0 1 0
If you really don't want to use this as a general business date calculation table, you can drop the last 3 columns, otherwise, update the Holiday column to 1 for company holidays.
Note: if you actually make the above table, and your access to it most often uses JOIN or WHERE conditions on a different column than BusinessDate, then make the primary key nonclustered and add a clustered index starting with the alternate column.
Some of the above scripts require SQL 2005 or higher.
It would be relatively easy to setup a custom calendar table with one row for each date of the year in it, and then have other fields that will allow you to rollup however you want. I do this when I have clients using varying calendars, i.e. fiscal years, and it makes the query logic very simple.
Then you just join date-to-date and get the week-number that you want.
date | reporting year | reporting week
-----------|----------------|---------------
2009-01-01 | 2008 | 52
2009-01-02 | 2008 | 52
2009-01-03 | 2008 | 52
2009-01-04 | 2009 | 01
2009-01-05 | 2009 | 01
etc.
and then to use it ( for example to get total sales rollup by your custom weeks, didn't validated my sql):
select reporting_year, reporting_month, sum(sales)
from sales
inner join custom_date_table cdt on cdt.sysdate = sales.sysdate
group by reporting_year, reporting_month
where report_year=2009
DECLARE #StartDate DATE;
SET #StartDate = '20120101';
WITH Calendar AS (
SELECT #StartDate AS DateValue
,DATEPART(DW, #StartDate) AS DayOfWeek
,CASE WHEN DATEPART(DW, #StartDate) = 1 THEN 1 ELSE 0 END AS WeekNumber
UNION ALL
SELECT DATEADD(d, 1, DateValue)
,DATEPART(DW, DATEADD(d, 1, DateValue)) AS DayOfWeek
,CASE WHEN DayOfWeek = 7 THEN WeekNumber + 1 ELSE WeekNumber END
FROM Calendar
WHERE DATEPART(YEAR, DateValue) = DATEPART(YEAR, #StartDate)
)
SELECT DateValue, WeekNumber
FROM Calendar
WHERE WeekNumber BETWEEN 1 AND 52
AND DATEPART(YEAR, DateValue) = DATEPART(YEAR, #StartDate)
OPTION (MAXRECURSION 0);
Don't use a UDF, use a calendar table instead, then you can define week numbers exactly as your company requires and simply query them from the table, which will be much easier and possibly much faster than using a UDF.
A calendar table has numerous uses in SQL (search this site or Google) so you should probably create it anyway.
There is no good answer for this.
A year is NOT 52 weeks long.
It is 52 weeks and one day in normal years, and 52 weeks and two days in leap years.

Resources