Get weekStart and weekEnd dynamically? - sql

I have spent so much time trying to figure out how this can be done but could not do it. So please help me out.
I have this data:
ID employee_id worked_date start_time finish_time
1 1 2013-09-25 09:00:00 17:30:00
2 1 2013-09-26 07:00:00 17:00:00
8 1 2013-10-01 09:00:00 17:00:00
9 1 2013-10-04 09:00:00 17:00:00
12 1 2013-10-07 09:00:00 17:00:00
13 1 2013-10-10 09:00:00 17:00:00
14 1 2013-10-11 09:00:00 17:00:00
My first day of the week is Wednesday. The base date is 2013-09-25 which is Wednesday. I need to be able to get the weekStart and weekEnd dynamically. For example, based on the data I have above, there are three weeks between 2013-09-25 to 2013-10-11. I order my data with the latest worked_date first. If the user requested for week3, then the weekStart should be 2013-09-25 and weekEnd should be 2013-10-01. If the user requested for week2, then the weekStart should be 2013-10-02 to 2013-10-08 and so on.
The parameter week will be passed dynamically. Thanks for your help.
Here is the result I want to achieve when the requested week is 3:
ID employee_id worked_date start_time finish_time weekStart weekEnd
1 1 2013-09-25 09:00:00 17:30:00 2013-09-25 2013-10-01
2 1 2013-09-26 07:00:00 17:00:00 2013-09-25 2013-10-01
8 1 2013-10-01 09:00:00 17:00:00 2013-09-25 2013-10-01
Using this query will give me the weekStart and weekEnd for each record
SELECT *, dateadd(week, datediff(day,'20000105',worked_date) / 7, '20000105') AS WeekStart ,
dateadd(week, datediff(day,'20000105',worked_date) / 7, '20000105') + 6 AS WeekEnd
FROM Timesheet
But I do not want this. I want only a particular week that the user requested.

It sounds like you might need a calendar table.
create table calendar (
calendarId int identity(1,1) primary key,
year int,
month int,
week int,
startDate date,
endDate date)
then you could say 'give me all the records for week 2'
declare #week int = 2
declare #year int = 2013
;with employeeCalendar as (
select
employee.employeeid
,startDate
,endDate
from
employee
cross apply
calendar
where
calendar.week = #week )
select
employeeCalendar.EmployeeId
,employeeShift.[date]
,start_time
,finish_time
,startDate
,endDate
from
employeeCalendar
left join
employeeShift
on employeeShift.employeeid = employeeCalendar.employeeid
and employeeShift.worked_date
between employeeCalendar.startDate and employeeCalendar.endDate
You could populate that table with a query like this:
;with calendarCte as (
select
1 as week
,convert(date,'2013-09-25') as startDate
,convert(date,'2013-10-01') as endDate
union all
select
week + 1
,dateadd(week,1,startDate)
,dateadd(week,1,endDate)
from
calendarCte
where
calendarCte.startDate < convert(date,'2043-09-25') )
insert into calendar( [year],[month],[week],startdate,stopdate)
select
datepart(year,startDate) as [year]
,datepart(month,startDate) as [month]
,week as [week]
,startDate
,endDate
from calendarCte option (maxrecursion 0)

You said you are looking for a stored procedure. Are you looking for something like this?
CREATE PROCEDURE ProcedureName #week int AS
SELECT * FROM Timesheet
WHERE worked_date >= dateadd(week, #week, '2013-09-25')
AND worked_date < dateadd(day,7,dateadd(week, #week, '2013-09-25'))
If you want your weekStart and weekEnd you could add them in to the SELECT as you did before.
If you want this in descending order from the current week backwards you could write it as follows.
CREATE PROCEDURE ProcedureName #week int AS
SELECT * FROM Timesheet
WHERE worked_date >= dateadd(week, -#week, '2013-10-16')
AND worked_date < dateadd(day,7,dateadd(week, -#week, '2013-10-16'))
You could also parameterize the Date you are passing into the stored procedure as the current week will always be changing.

Related

How to Convert a Date Span to Monthly Records using SQL

I have multiple date spans for the user over a period of few months, I would like to split each span to multiple rows by month and year(default to first day of the month) for which user has been active during the span period. Active user will have future end date records to be split up until the current month and year
Existing Data
ID
Start date
end date
1234
2019-01-01
2019-03-31
1234
2019-09-18
2020-01-31
1234
2022-11-15
2025-01-31
Tried to place the below date month query into the spans
Select Top 500 mmdd=cast (dateadd(Month,-1+Row_Number() Over (Order By (Select NULL)),'2019-01-01') as date)
From master..spt_values n1
order by 1 asc
EXPECTED OUTPUT
ID
active month
1234
2019-01-01
1234
2019-02-01
1234
2019-03-01
1234
2019-09-01
1234
2019-10-01
1234
2019-11-01
1234
2019-12-01
1234
2020-01-01
1234
2022-11-01
1234
2022-12-01
1234
2023-01-01
Larnu is on the right track. One of the easiest ways I've found is to create a calendar table or a function (which can effectively do the same thing).
Try this:
CREATE FUNCTION [dbo].[udfCalendar]
(
#StartDate Date,
#EndDate Date
)
RETURNS #Calendar TABLE (ID int, DateValue DateTime, DayValue int, MonthValue int, YearValue int)
AS
BEGIN
WHILE #StartDate < #EndDate
BEGIN
INSERT #Calendar
SELECT --like 20190101, 1/1/2019, 1, 1, 2019
YEAR (#StartDate) * 10000 + MONTH (#StartDate) * 100 + Day (#StartDate) AS ID,
#StartDate AS DateValue,
DATEPART (dd, #StartDate) AS DayValue,
DATEPART (mm, #StartDate) AS MonthValue,
DATEPART (yy, #StartDate) AS YearValue;
SET #StartDate = DateAdd(m, 1, #StartDate);
END
RETURN;
END
Then you can join to it
Select n1.ID, cal.DateValue as ActiveMonth
From master..spt_values n1 inner join
dbo.udfCalendar('1/1/2019', '1/1/2023') cal
On cal.DateValue Between n1.StartDate and n1.EndDate
Order By DateValue

Query based on day-of-week/time range

I'm on SQL Server 2014. I have a table with columns like this:
id (int, PK, identity)
effectiveDate (datetime)
expirationDate (datetime)
1
2022-07-06 18:00:00.000
2022-07-06 23:00:00.000
2
2022-07-08 22:00:00.000
2022-07-09 02:00:00.000
I need to select rows where the current date/time (GETDATE()) lands within the day-of-week/time range represented by these datetimes, beginning on the effectiveDate. So think of row 1 as the range Wednesday 18:00 -> Wednesday 23:00 and row 2 as Friday 22:00 -> Saturday 02:00. (Keep in mind the day-of-week/time range can span multiple days, as in the 2nd row.)
Examples:
GETDATE() = 2022-07-06 19:30:00.000 (i.e Wednesday at 19:30)
Selects row 1
GETDATE() = 2022-07-30 01:00:00.000 (i.e. Saturday at 01:00)
Selects row 2
GETDATE() = 2022-06-30 19:00:00.000 (i.e. Wednesday at 19:00 which matches row 1 on the day-of-week/time range, but is before the effective date)
Selects nothing
I'm not quite sure how to attack this. Maybe we could date adjust each row's effectiveDate and expirationDate as many weeks forward as needed to place the effectiveDate before GETDATE() (assuming the effectiveDate <= GETDATE()). Any thoughts?
The DATEPART with weekday is the key to checking the weekday range. There are two cases. For example, Mon to Wed is different than Wed to Mon. Mon to Wed is easy with a test of Mon<=day<= Wed. For Wed to Mon, it becomes day < Mon or day > Wed. However, this is the same as NOT Mon<=day<=Wed. The query might not be exactly what you need, but should be a good start.
with TestData as (
select *
from (
values
(1, CAST('2022-07-06 18:00:00.000' as datetime), CAST('2022-07-06 23:00:00.000' as datetime)),
(2, CAST('2022-07-08 22:00:00.000' as datetime), CAST('2022-07-09 02:00:00.000' as datetime)),
(3, CAST('2022-07-09 22:00:00.000' as datetime), CAST('2022-07-11 10:00:00.000' as datetime))
) t (id, effectiveDate, expirationDate)
), TestSamples as (
select *
from (
values
(CAST('2022-07-06 19:30:00.000' as datetime)),
(CAST('2022-07-30 01:00:00.000' as datetime)),
(CAST('2022-07-31 01:00:00.000' as datetime)),
(CAST('2022-06-30 19:00:00.000' as datetime)),
(CAST('2022-08-04 19:00:00.000' as datetime))
) t (testDate)
), WeekDayRange as (
select id, effectiveDate, expirationDate,
datename(weekday, effectiveDate) as WDNAME1, datename(weekday, expirationDate) as WDNAME2,
datepart(weekday, effectiveDate) as WD1, datepart(weekday, expirationDate) as WD2
from TestData
) select t.testDate, datename(weekday, t.testDate) as [WDNAME], datepart(weekday, t.testDate) as [WD], r.*
from TestSamples t
left join WeekDayRange r
on (r.WD1 <= r.WD2 and datepart(weekday, t.testDate) BETWEEN r.WD1 and r.WD2)
or (r.WD1 > r.WD2 and not datepart(weekday, t.testDate) BETWEEN r.WD2 and r.WD1)
where t.testDate > r.effectiveDate
testDate WDNAME WD id effectiveDate expirationDate WDNAME1 WDNAME2 WD1 WD2
----------------------- --------- ----------- ----------- ----------------------- ----------------------- --------- --------- ----------- -----------
2022-07-06 19:30:00.000 Wednesday 4 1 2022-07-06 18:00:00.000 2022-07-06 23:00:00.000 Wednesday Wednesday 4 4
2022-07-30 01:00:00.000 Saturday 7 2 2022-07-08 22:00:00.000 2022-07-09 02:00:00.000 Friday Saturday 6 7
2022-07-31 01:00:00.000 Sunday 1 3 2022-07-09 22:00:00.000 2022-07-11 10:00:00.000 Saturday Monday 7 2
I think I got this working with a bit of math:
DECLARE #myGetDate DATETIME;
SET #myGetDate = '2022-07-30 01:00:00.000';
WITH AdjustedWeeklyDates AS (
SELECT
ID,
DATEADD(WEEK, DATEDIFF(day, effectiveDate, #myGetDate) / 7, effectiveDate) AS adjustedEffectiveDate,
DATEADD(WEEK, DATEDIFF(day, effectiveDate, #myGetDate) / 7, expirationDate) AS adjustedExpirationDate
FROM
dbo.myTable
WHERE
effectiveDate <= #myGetDate
)
SELECT
mt.ID, mt.effectiveDate, mt.expirationDate, awd.adjustedEffectiveDate, awd.adjustedExpirationDate
FROM dbo.myTable mt
INNER JOIN AdjustedWeeklyDates awd ON mt.ID = awd.ID
WHERE
awd.adjustedEffectiveDate <= #myGetDate
AND awd.adjustedExpirationDate > #myGetDate
To explain:
DATEDIFF(day, effectiveDate, #myGetDate) returns the number of days between the effectiveDate and the current date. So for example, say it was 20 days ago.
/ 7 gets the number of weeks as an int, since the DATEDIFF returns an int. This also results in the floor of the quotient. So, with our example 20 days / 7, the quotient is about 2.86, but this will result in an even 2
DATEADD adds the number of weeks to bring us up to or before the current date/time. We add the same number of weeks to the expiration date, resulting in the same range as the original effective/expiration dates, which may or may not extend around the current date.
Finally the check for effectiveDate <= #myGetDate guarantees the current date is after or equal to the effective date.

Find all instances of a specific day of the week between two dates in SQL

I'm trying to find all instances of a day of a week between two given dates. The day of the week can change. I've seen a similar question posted on here, but it doesn't seem to work for variables
SET DATEFIRST 1;
DECLARE #earliestStartDate DATETIME = '2016-08-01 00:00:00.000';
DECLARE #latestStartDate DATETIME = '2016-09-30 00:00:00.000';
DECLARE #weeklyCoursesStartDay INT = 1;
DECLARE #maxCourses INT = 30;
CREATE TABLE #Dates(CourseDate DATETIME);
WITH CTE(dt)
AS
(SELECT #earliestStartDate
UNION ALL
SELECT DATEADD(day, #weeklyCoursesStartDay, dt) FROM CTE--SELECT DATEADD(day, #weeklyCoursesStartDay, dt) FROM CTE
WHERE dt < #latestStartDate)
INSERT INTO #Dates(CourseDate)
SELECT TOP(#maxCourses) dt
FROM CTE
WHERE DATEPART(DW, dt) = #weeklyCoursesStartDay
OPTION (MAXRECURSION 0);
SELECT * FROM #Dates
DROP TABLE #Dates
This returns the below, as expected.
2016-08-01 00:00:00.000
2016-08-08 00:00:00.000
2016-08-15 00:00:00.000
2016-08-22 00:00:00.000
2016-08-29 00:00:00.000
2016-09-05 00:00:00.000
2016-09-12 00:00:00.000
2016-09-19 00:00:00.000
2016-09-26 00:00:00.000
But when the #weeklyCourseStartDay is changed to say, 5 (indicating a friday) it only returns two results:
2016-08-26 00:00:00.000
2016-09-30 00:00:00.000
The line
SELECT DATEADD(day, #weeklyCoursesStartDay, dt) FROM CTE
should actually be:
SELECT DATEADD(day, 1, dt) FROM CTE
This means that the recursive CTE will generate every day between #earliestStartDate and #latestStartDate, and, at a later stage, the WHERE DATEPART(DW, dt) = #weeklyCoursesStartDay clause will make sure only the days with the correct week day will remain.

SQL - creating a list of custom dates between two dates

I am having trouble compiling a query than can do the following:
I have a table which has a startDate and endDate [tblPayments]
I have a column which stores a specific paymentDay [tblPayments]
Data
paymentID startDate endDate paymentDay
1 2016-01-01 2016-12-31 25
2 2015-06-01 2016-06-30 16
I am trying to generate a SELECT query which will split this specific table into separate lines based on the amount of months between these two dates, and set the paymentDay as the day for these queries
Example Output
paymentID expectedDate
1 2016-01-25
1 2016-02-25
1 2016-03-25
1 2016-04-25
1 2016-05-25
1 2016-06-25
1 2016-07-25
1 2016-08-25
1 2016-09-25
1 2016-10-25
1 2016-11-25
1 2016-12-25
2 2015-06-16
2 2015-07-16
2 2015-08-16
2 2015-09-16
2 2015-10-16
2 2015-11-16
2 2015-12-16
2 2016-01-16
2 2016-02-16
2 2016-03-16
2 2016-04-16
2 2016-05-16
I have found a query which will select the months between these dates but its adapting it to my table above, and multiple startDates and endDates I am struggling with
spliting the months
declare #start DATE = '2015-01-01'
declare #end DATE = '2015-12-31'
;with months (date)
AS
(
SELECT #start
UNION ALL
SELECT DATEADD(MM,1,date)
from months
where DATEADD(MM,1,date)<=#end
)
select Datename(MM,date) from months
This query is limited to just one startDate and endDate, so I haven't expanded it to change the DAY of the date.
Use a date table and a simple inner join
DECLARE #tblPayments table (paymentID int identity(1,1), startDate date, endDate date, paymentDay int)
INSERT #tblPayments VALUES
('2016-01-01', '2016-12-31', 25),
('2015-06-01', '2016-06-30', 16)
;WITH dates AS -- Build date within the range of startDate and endDate
(
SELECT MIN(startDate) AS Value, MAX(endDate) AS MaxDate FROM #tblPayments
UNION ALL
SELECT DATEADD(DAY, 1, Value), MaxDate
FROM dates WHERE DATEADD(DAY, 1, Value) <= MaxDate
)
SELECT pay.paymentID, dates.Value AS expectedDate
FROM
#tblPayments pay
INNER JOIN dates ON
dates.Value BETWEEN pay.startDate AND pay.endDate
AND DAY(dates.Value) = paymentDay
OPTION (maxrecursion 0)
I would create an in memory calendar table and then perform a simple query by joining to that:
-- Create a table with all the dates between the min and max dates in the
-- data table
DECLARE #Calendar TABLE
(
[CalendarDate] DATETIME
)
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SELECT #StartDate = MIN(startdate), #EndDate = MAX(enddate) FROM YourDataTable
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #Calendar (CalendarDate)
SELECT #StartDate
SET #StartDate = DATEADD(dd, 1, #StartDate)
END
-- Join to return only dates between the start and end date that match the Payment Day
SELECT D.PaymentId, C.CalendarDate FROM YourDataTable D
INNER JOIN #Calendar C ON C.CalendarDate BETWEEN D.StartDate AND D.EndDate
AND DATEPART(day, C.CalendarDate) = D.PaymentDay

Breaking out yearly payments into monthly payments with month name in a 3 year period

I was wondering where to go from my initial idea. I used the query below to get the month beginning dates for each of the three years:
DECLARE #STARTDATE DATETIME,
#ENDDATE DATETIME;
SELECT #STARTDATE='2013-01-01 00:00:00.000',
#ENDDATE='2015-12-31 00:00:00.000';
WITH [3YearDateMonth]
AS
(
SELECT TOP (DATEDIFF(mm,#STARTDATE,#ENDDATE) + 1)
MonthDate = (DATEADD(mm,DATEDIFF(mm,0,#STARTDATE) + (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1),0))
FROM sys.all_columns ac1
)
SELECT MonthDate
FROM [3YearDateMonth]
I am not sure if I should DATENAME(Month, Monthdate) it later for the month names or just do it in the cte; any suggestions would be great.
My data looks like this:
BeginDate EndDate Payment
2013-01-01 00:00:00.000 2013-12-31 00:00:00.000 3207.70
2014-01-01 00:00:00.000 2014-12-31 00:00:00.000 3303.93
2015-01-01 00:00:00.000 2015-12-31 00:00:00.000 3403.05
Since the payment is yearly I can use payment/12 to get an average monthly amount. I want my data to look like this:
BeginDate EndDate Month MonthlyAmount
2013-01-01 00:00:00.000 2013-01-31 00:00:00.000 January 267.3083
2013-02-01 00:00:00.000 2013-02-31 00:00:00.000 February 267.3083
...
2014-01-01 00:00:00.000 2014-01-31 00:00:00.000 January 275.3275
2014-02-01 00:00:00.000 2014-02-31 00:00:00.000 February 275.3275
...
2015-01-01 00:00:00.000 2015-01-31 00:00:00.000 January 283.5875
2015-02-01 00:00:00.000 2015-02-31 00:00:00.000 February 283.5875
All the way through December for each yearly pay period.
I will be pivoting the Month column later to put the monthly amounts under the corresponding month they belong in.
Is this doable because I feel lost at this point?
Starting with your three data rows, you can use the following query to get your desired results:
with months as
(
select BeginDate
, EndDate
, Payment = Payment / 12.0
from MyTable
union all
select BeginDate = dateadd(mm, 1, BeginDate)
, EndDate
, Payment
from months
where dateadd(mm, 1, BeginDate) < EndDate
)
select BeginDate
, EndDate = dateadd(dd, -1, dateadd(mm, 1, BeginDate))
, Month = datename(mm, BeginDate)
, MonthlyAmount = Payment
from months
order by BeginDate
SQL Fiddle with demo.
Here's a query for you:
WITH L1 (N) AS (SELECT 1 UNION ALL SELECT 1),
L2 (N) AS (SELECT 1 FROM L1, L1 B),
L3 (N) AS (SELECT 1 FROM L2, L2 B),
Num (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM L3)
SELECT
P.BeginDate,
P.EndDate,
M.MonthlyPayDate,
MonthlyAmount =
CASE
WHEN N.N = C.MonthCount
THEN P.Payment - Round(P.Payment / C.MonthCount, 2) * (C.MonthCount - 1)
ELSE Round(P.Payment / C.MonthCount, 2)
END
FROM
dbo.Payment P
CROSS APPLY (
SELECT DateDiff(month, BeginDate, EndDate) + 1
) C (MonthCount)
INNER JOIN Num N
ON C.MonthCount >= N.N
CROSS APPLY (
SELECT DateAdd(month, N.N - 1, BeginDate)
) M (MonthlyPayDate)
ORDER BY
P.BeginDate,
M.MonthlyPayDate
;
See a Live Demo at SQL Fiddle
Pluses:
Doesn't assume 12 months--it will work with any date range.
Properly rounds all non-ultimate months, then assigns the remainder to the last month so that the total sum is accurate. For example, for 2013, the normal monthly payment is 267.31, but December's month's payment is 267.29.
Minuses:
Assumes all dates entirely enclose full months, starting on the 1st and ending on the last day of the month.
If you provide more detail about further requirements regarding pro-rating, I can improve the query for you.