How can I get the last day of periodic months in SQL? - sql

I am preparing the next 5 months date according to the value given in the query I wrote.
DECLARE #StartDate DATETIME = '2022-03-31', #monthadd INT = 5;
; WITH dates AS (
SELECT #StartDate [vade]
UNION ALL
SELECT DATEADD(MONTH,1,[vade])
FROM dates
WHERE DATEADD(MONTH,1,[vade]) <= DATEADD(MONTH,#monthadd,#StartDate)
)
SELECT *
FROM dates
OPTION (MAXRECURSION 0)
GO
However, when the last day of the month is 31, it is necessary to list the last day, which is the nearest day, in the following months. how do i do this?
Actual results
vade
2022-03-31 00:00:00.000
2022-04-30 00:00:00.000
2022-05-30 00:00:00.000
2022-06-30 00:00:00.000
2022-07-30 00:00:00.000
2022-08-30 00:00:00.000
Edit:
This is a maturity plan. If the person makes installments on the 31st of the month, the payment must be made on the last day of each month. If he does it on the 30th, the month should have 30 if it has 30 days, 30 if it has 31 days, and 29 if it has 29 days. If maturity starts on the 20th, it must be the 20th of each month. Imagine you take out a loan on the 30th of the month. If the month is 29 days, they will ask you to pay on the 29th day, and if the month is 31 days, they will ask you to pay on the 30th day. I know it's very confusing and I'm sorry about that.

Updated 2022-04-01
If I'm understanding correctly, you want to return the same "day" for each month - except when #StartDate is the last day of the month.
One approach would be to determine if the #StartDate is the last day of the month. If so, use EOMONTH() to return the last day in each of the subsequent months. Otherwise, use DATEADD() to return the specified "day" in each month. This approach should work for any date.
One approach is as follows:
If Maturity Date is last day of month, OR Maturity Day of month is > number of days in subsequent month, use EOMONTH() to return the last day of that month
Otherwise, use DATEADD() and DATEFROMPARTS() to generate the next date using the Maturity Day of month
SQL:
-- Note: Using 12 months for demo only
; WITH dates AS (
SELECT #StartDate AS MaturityDate
, IIF(#StartDate = EOMONTH(#StartDate), 1, 0) AS IsEOM
UNION ALL
SELECT
CASE -- Maturity date is last day of month OR
-- Maturity "day" is > number of days in current month
WHEN IsEOM = 1 OR DAY(#StartDate) > DAY( EOMONTH(NextMaturityDate) )
THEN EOMONTH( DATEADD(MONTH, 1, MaturityDate ))
-- Otherwise, maturity "day" is valid for current month
ELSE DATEFROMPARTS(
Year(NextMaturityDate)
, Month(NextMaturityDate)
, DAY(#StartDate)
)
END
, IsEOM
FROM ( SELECT MaturityDate
, IsEOM
, DATEADD(MONTH, 1, MaturityDate) AS NextMaturityDate
FROM dates
) t
WHERE MaturityDate < #EndDate
)
SELECT MaturityDate AS [vade]
FROM dates
OPTION (MAXRECURSION 0)
Results for 2022-03-31 (Last Day Of Month)
vade
2022-03-31
2022-04-30
2022-05-31
2022-06-30
2022-07-31
2022-08-31
2022-09-30
2022-10-31
2022-11-30
2022-12-31
2023-01-31
2023-02-28
2023-03-31
2023-04-30
2023-05-31
2023-06-30
Results for 2022-03-30 (NOT Last Day Of Month)
vade
2022-03-30
2022-04-30
2022-05-30
2022-06-30
2022-07-30
2022-08-30
2022-09-30
2022-10-30
2022-11-30
2022-12-30
2023-01-30
2023-02-28
2023-03-30
2023-04-30
2023-05-30
2023-06-30
db<>fiddle here

The DATEADD function already takes into account of the corner cases, like the end of the month, so you don't need to handle it.
In order to have a cleaner code, you can lay down a stored procedure, that creates (or replaces) a dates_list table and then cycles over the number of months to add to the start date.
DELIMITER //
CREATE OR REPLACE PROCEDURE create_dates_list (
IN start_date DATETIME,
IN num_months INT
)
BEGIN
DECLARE idx INT DEFAULT 0;
CREATE OR REPLACE TABLE dates_list (
date DATE
);
WHILE idx <> num_months DO
INSERT INTO tab VALUES(
DATEADD(#start_date, INTERVAL #idx MONTH)
);
SET idx = idx + 1;
END WHILE;
END //
DELIMITER ;
When you need to obtain new dates, you can refresh that table by setting the parameters and calling the stored procedure:
DECLARE #StartDate DATETIME = '2022-03-31', #monthadd INT = 5;
CALL create_dates_list(#StartDate, #monthadd);
You can freely access the table anytime by using the tools that sql empowers you with.
If you don't need the table to exist for further sessions, you can define the table as TEMPORARY. The official documentation on temporary tables is very detailed and comprehensive of examples, check it out to get to know more about it.

Related

How to convert weekly data into monthly data in sql?

I have a table that has weekly effort hrs and I need to report data on a monthly basis. for most of the cases, the week start date and end date lie in the same month and those records are easy to aggregate but when the week start date and end date don't lie in the same month then I need to break that week's records into 2 rows. One with an actual start date and end of the month as the end date and another record as 1st of next month as the start date and actual end date as the end date.
For the hrs calculation, I need to calculate the number of days that fall in the first month then divide the total hrs by 5 and then multiply by the number of days. and for the second record, it will be total hrs by 5 and then multiply by (5-number of days)
SELECT [USERNAME]
,[EMPLOYEE_NAME]
,[EFFORT_HRS]
,[TS_START_DATE]
,[TS_END_DATE]
FROM [dbo].[Source]
OUTPUT:
USERNAME
EMPLOYEE_NAME
Task
EFFORT_HRS
TS_START_DATE
TS_END_DATE
mk
xyz
abcdefg
40
12/27/2021
1/2/2022
mk
xyz
defgh
33.5
1/31/2022
2/6/2022
mk
xyz
abcdefg
6
4/25/2022
5/1/2022
Expected Result:
USERNAME
EMPLOYEE_NAME
Task
EFFORT_HRS
TS_START_DATE
TS_END_DATE
mk
xyz
abcdefg
40
12/27/2021
12/31/2021
mk
xyz
abcdefg
0
1/1/2022
1/2/2022
mk
xyz
defgh
6.7
1/31/2022
1/31/2022
mk
xyz
defgh
26.8
2/1/2022
2/6/2022
mk
xyz
abcdefg
6
4/25/2022
4/30/2022
mk
xyz
abcdefg
0
5/1/2022
5/1/2022
What you are NOT providing is some sort of calendar associated with holidays such as Jan 1, 2021 (Friday), and Jan 2, 2021 (Saturday) which do not appear to be paid out. That being said, I will leave you to either provide additional information to exclude such dates, or figure out how to adjust the query.
I would also suggest a better storage solution for time be done on a daily basis for easier querying and inclusion / exclusion such as holidays. But, from what is given I have below sample to create and populate table per your sample data.
create table Source
( UserName nvarchar(5),
Employee_Name nvarchar(5),
Task nvarchar(10),
Effort_Hrs numeric( 5, 2),
TS_Start_Date datetime,
TS_End_Date datetime )
insert into Source
( UserName,
Employee_Name,
Task,
Effort_Hrs,
TS_Start_Date,
TS_End_Date)
values
( 'mk', 'xyz', 'abcdefg', 40, '2021-12-27', '2022-01-02' ),
( 'mk',' xyz', 'defgh', 33.5, '2022-01-31', '2022-02-06' ),
( 'mk',' xyz', 'abcdefg', 6, '2022-04-25', '2022-05-01' )
And now the query itself. No matter what you do based of the existing data, you will need a UNION. Basically, selecting the same columns of data in each query. First, get all records once, but chop-off the end date to the last of the month if the start and end dates are different months.
The UNION part will only consider those records where the start and end dates are DIFFERENT months. So, if you had a work week of Jan 5 - Jan 11, you would only see it in the single record with no split, but in your other examples that cross months, you get TWO records. One for the first month, one for the second.
I have rewritten the query based on your feedback of week days and ignoring that of weekends. As such, when computing the pay earned, I had to apply a case/when if PayDays = 0, to just return 0 hours, otherwise you would get a divide by zero error
I am adding extra columns (which you can remove), so you can see how / where the pieces come into play. Now, for ALL records, the start date IS the real basis of the start date in final output. In the first query, its straight-forward, it IS the start date. However, in the UNION portion query, the start date is the first of the month, but again, only in the second query the months are different. So for that, I am doing date add (net subtract) 1 less than the day of the month of the ending date.
I also changed to use EOMONTH() call to compute the end of month for a given date such as in the first part of union where the end date crosses into following month vs the dateadd() originally used.
For the pay days in the UNION part of the query, which only represents entries that cross into the following month, the PayDays IS the ending date Days. So Feb 6th would be 6 days.
NOT month( ts_start_Date ) = month( ts_end_Date )
Here is a function to compute your work days within a given time period begin/end dates that forces cycle through each individual day to determine Mon-Fri.
CREATE Function dbo.WorkDaysInWeek
( #pStartDate as datetime,
#pEndDate as datetime)
RETURNS int
AS
BEGIN
-- if bad start/end date because they passed in end date first, swap them
if( #pStartDate > #pEndDate )
begin
declare #holdDate as DateTime
set #holdDate = #pStartDate
set #pStartDate = #pEndDate
set #pEndDate = #holdDate
end
-- convert to just date to prevent false calculations on time consideration
set #pStartDate = convert( date, #pStartDate)
set #pEndDate = convert( date, #pEndDate )
declare #workDays as int
set #workDays = 0
WHILE ( #pStartDate <= #pEndDate)
BEGIN
-- is the current day being tested a week day vs weekend. Only count Mon-Fri
if( datepart( weekday, #pStartDate) in ( 2, 3, 4, 5, 6 ))
set #workDays = #workDays + 1
set #pStartDate = dateadd( day, 1, #pStartDate )
END
RETURN #workDays
end
GO
Now, instead of computing the datediff in days, you call the function with begin and end dates and the function will cycle through each day individually to determine Mon-Fri only to be counted
Here is the final query.
select
AllWork.*,
case when AllWork.PayDays = 0
then 0.0
else ( AllWork.EFFORT_HRS / ( 1.0 * dbo.WorkDaysInWeek( AllWork.TS_Start_Date, AllWork.TS_End_Date))) * AllWork.PayDays end HoursInPayPeriod
from
(SELECT
USERNAME,
EMPLOYEE_NAME,
EFFORT_HRS,
TS_START_DATE,
TS_END_DATE,
TS_START_DATE as RealStart,
case when month( ts_start_Date ) = month( ts_end_Date )
then ts_end_date
-- simplified date add to use build-in function EOMONTH (End of Month)
else EOMONTH( ts_start_Date, 0) end RealEnd,
dbo.WorkDaysInWeek( TS_START_DATE,
case when month( ts_start_Date ) = month( ts_end_Date )
then ts_end_date
-- simplified date add to use build-in function EOMONTH (End of Month)
else EOMONTH( ts_start_Date, 0) end ) PayDays
FROM
Source
UNION
-- union to get all entries where the end date is a new month from the start
SELECT
USERNAME,
EMPLOYEE_NAME,
EFFORT_HRS,
TS_START_DATE,
TS_END_DATE,
dateadd( day, 1 - datepart( day, ts_end_Date ), ts_end_Date ) RealStart,
ts_end_date RealEnd,
dbo.WorkDaysInWeek( dateadd( day, 1 - datepart( day, ts_end_Date ), ts_end_Date ),
ts_end_date ) PayDays
FROM
Source
where
-- only care about those entries where the start and end date are different months
NOT month( ts_start_Date ) = month( ts_end_Date )
) AllWork

I need Dynamic query in BigQuery to find on which Date the Day Saturday has occurred in First week of Year 2021

I need Dynamic query in BigQuery to find Date of Day Saturday has occurred in First week of Year 2021.
Consider the Day on date of 2022-01-01 is Saturday.
Therefore, I want to extract the date of last Year first week on which the Saturday was occurred.
Try the following:
with sample_dates as (
select date
from unnest(generate_date_array('2022-01-01','2022-12-31')) as date
)
select
date
,(select last_year_date
from unnest(generate_date_array(date_sub(date, INTERVAL 1 YEAR), date-1)) as last_year_date
where extract(week from date) = extract(week from last_year_date)
and extract(dayofweek from date) = extract(dayofweek from last_year_date)
) as last_year_date
from sample_dates
;
Given all of 2022 dates, this provides the same day of week for the same week in the previous year.
For example 2022-01-01 results in 2022-01-02 which is the first Saturday of the first week of last year.
If you're only looking for a single date the main portion would be the generating the appropriate date range with the generate_date_array function and then filtering based on the same week number and dayofweek value.
Use below - should work for any year - just replace 2021 with whatever year you need
select date(2021, 1, 8 - extract(dayofweek from date(2021, 1, 1)))
I think this code can help you!
You can find the first Saturday with a weekly cycle
declare
i number := 0;
date1 date := date '2022-01-01';
d varchar2(20);
begin
while i < 7 loop
SELECT case
when TO_CHAR(date1 + i, 'fmDay') = 'Saturday' then
date1 + i
end "Day"
into d
FROM DUAL;
i := i + 1;
dbms_output.put_line(d);
end loop;
end;

How to get all the dates in a full calendar month

In a calendar control, we can see some dates from the previous month and next month also. Sample image below
(ie Apr-2016: Starts from Mar-28 and ends in May-08
Mar-2016: Starts from Apr Feb-29 and ends in Apr-10)
Here, i need to generate a list of all the dates in a calendar control for a particular year month. My week start is Monday.
Here is the tsql script i have tried so far.
DECLARE #V_DATE DATE = GETDATE()
;WITH CTE_DATE AS (
SELECT DATEADD(dd,-(DAY(#V_DATE)-1),#V_DATE) CDATE
UNION ALL
SELECT DATEADD(dd,1,CDATE)
FROM CTE_DATE
WHERE DATEADD(dd,1,CDATE) <= DATEADD(dd,-(DAY(DATEADD(mm,1,CDATE))),DATEADD(mm,1,CDATE))
)
SELECT * FROM CTE_DATE
Result Is:
2016-04-01
2016-04-02
.
.
2016-04-29
2016-04-30
It will list all the days from a inputted year month, but i need to include the
missing dates from the previous month as well as next month.
Expected result for Apr-2016
2016-03-28
2016-03-29
.
2016-04-15
.
2016-05-07
2016-05-08
Expected result for May-2016
2016-04-25
2016-04-26
.
2016-05-15
.
2016-06-04
2016-06-05
Note:- The calendar control is always showing 42 days.
since your week is starts on Monday,you can take referece to date 0 '1900-01-01' which is a Monday. Adding 41 days would gives you your end date
select
date_fr = dateadd(day, datediff(day, 0, '2016-05-01') / 7 * 7, 0),
date_to = dateadd(day, datediff(day, 0, '2016-05-01') / 7 * 7, 41)
the following gives you date 1900-01-01 and Monday
select convert(datetime, 0), datename(weekday, 0)
Have you considered creating a dates table in your database. You would have columns for dates and a column for week number. Linking to this table you could find the week number for your start and end dates, you could then re-link to the table to find the first date of your start week and the last date of your end week. This would probably be more efficient than calculations at each step each time, it is a simple link.
I have create done script for this. This is working as per my expectation, may be helpful for future reference. (Thanks #Squirrel for the logic).
DECLARE #V_ST_DATE DATE = GETDATE()
SET #V_ST_DATE = DATEADD(DAY,-(DAY(#V_ST_DATE)-1),#V_ST_DATE)
SET #V_ST_DATE = DATEADD(WEEK,DATEDIFF(WEEK, 0, #V_ST_DATE) ,0) +
(CASE WHEN DATEADD(WEEK,DATEDIFF(WEEK, 0, #V_ST_DATE) ,0) > #V_ST_DATE THEN -7 ELSE 0 END)
;WITH CTE_DATE AS (
SELECT #V_ST_DATE CDATE,0 TDAYS
UNION ALL
SELECT DATEADD(DAY,1,CDATE) , DATEDIFF(DAY,#V_ST_DATE, DATEADD(DAY,1,CDATE))
FROM CTE_DATE
WHERE DATEDIFF(DAY,#V_ST_DATE, DATEADD(DAY,1,CDATE)) < 42
)
SELECT * FROM CTE_DATE

Given a date, select all the week numbers that month contains

I have an ApEx application for tracking working hours.
I have a view that looks like this:
CREATE OR REPLACE VIEW HOURSDAYS
AS SELECT
(MAX("TO_X") - MIN("FROM_X"))*24 -
(max(case when PROJECT_ID = 999 then to_x else to_date('01012000','DDMMYYYY') end) -
max(case when PROJECT_ID = 999 then from_x else to_date('01012000','DDMMYYYY') end))*24 AS TIME_SPENT,
DAY,
PERSON_ID
FROM ATTENDANCE_HOURS
GROUP BY PERSON_ID, DAY
ORDER BY DAY DESC
I need the sum of hours per week. So I have this:
SELECT TO_CHAR(DAY,'IW'), MIN(DAY), MAX(DAY), SUM(TIME_SPENT)
FROM HOURSDAYS
WHERE PERSON_ID = (SELECT ID FROM ATTENDANCE_PEOPLE WHERE MAIL_SSO = V('APP_USER') AND ROLE = 0) AND
EXTRACT (YEAR FROM DAY) = EXTRACT (YEAR FROM TO_DATE(:P100_DATE_PICKER,'DD-MON-RR'))
GROUP BY TO_CHAR(DAY,'IW')
ORDER BY TO_CHAR(DAY,'IW') ASC
And now the fun begins: I have a page item P100_DATE_PICKER and I need to display only those weeks that have > 0 days that belong to the month to which belongs the day I've picked using the date picker.
For example, for 1.1.2015 I want only weeks 1, 2, 3, 4, 5 displayed. 24.3.2015: 9, 10, 11, 12, 13, 14.
If anyone is interested in why would I do that, it is for validation - number of work hours per week cannot exceed 20/32, depends on what type of contract do you have.
You can do like this:
where day >= trunc(trunc(TO_DATE(:P100_DATE_PICKER,'DD-MON-RR'), 'MM'),'IW')
and day < trunc(last_day(TO_DATE(:P100_DATE_PICKER,'DD-MON-RR')),'IW') + 7
The first line starts by trunc to MM, which gives you the first day of the month of your date. Then trunc again to IW gives you the date of the monday in the week that contains the first day of month.
The second line uses last_day to get the last day of the month of your date. Then trunc to IW gets the monday of that week, and adding 7 days get the monday after. Then by using < rather than <= you get the desired result.
Also I suggest you do not group and order by TO_CHAR(DAY,'IW') but instead use TRUNC(DAY,'IW') for your grouping and ordering. Otherwise you can get into problems around new year, because your code for example when showing the weeks for December 2014 would have to show 49 to 52 of year 2014 as well as week 1 of year 2015. If you use TO_CHAR that week 1 would be wrongly sorted before weeks 49 to 52.
Similarly do not use EXTRACT(YEAR FROM ...) or even TO_CHAR(...,'YYYY') when you are working with ISO weeks. Because the date '2014-12-31' belongs to ISO week 1 year 2015, there is a special date format string 'IYYY' which gives the correct year for the ISO week. Try it out and see the difference between 'YYYY' and 'IYYY'.
This will not answer all your questions. This is the quick example of week calculation from a given date according to the subject line of your question. Pls copy/paste to see results. Also, here's the link to check the week numbers:
http://www.epochconverter.com/date-and-time/weeknumbers-by-year.php
SELECT given_date
, end_date
, TRUNC(calc_start_date, 'iw') wk_starts
, TRUNC(calc_start_date, 'iw') + 7 - 1/86400 wk_ends
, TO_CHAR(calc_start_date, 'iw') wk_number
, calc_start_date
FROM
(
SELECT trunc(sysdate, 'mm') given_date
, trunc(sysdate, 'mm')-7 + LEVEL*7 AS calc_start_date
, Last_Day(trunc(SYSDATE, 'mm')) end_date
FROM dual
CONNECT BY LEVEL <= ROUND((trunc(last_day(sysdate)) - trunc(sysdate, 'mm')+7)/7) -- number of weeks --
)
/
GIVEN DATE END DATE WK_STARTS WK_ENDS WK NUMBER
-------------------------------------------------------------------------
1/1/2015 1/31/2015 12/29/2014 1/4/2015 11:59:59 PM 01
1/1/2015 1/31/2015 1/5/2015 1/11/2015 11:59:59 PM 02
1/1/2015 1/31/2015 1/12/2015 1/18/2015 11:59:59 PM 03
1/1/2015 1/31/2015 1/19/2015 1/25/2015 11:59:59 PM 04
1/1/2015 1/31/2015 1/26/2015 2/1/2015 11:59:59 PM 05
The best way to achieve the last part is to have a calendar table that has the following columns
Date, Day eg Mon, Month, Quarter, Year, WeekNo
Then for a given date you can simply select all the weeks that belong to the month.
This allows you to define weekNo so that it fits in with company circumstances. Eg occasionally you may get a 53 week year, especially if they operate 4-4-5 accounting periods
with days as (
select
to_number(to_char(trunc(to_date('2015-03-24', 'yyyy-MM-dd'), 'month'), 'WW')) w1,
to_number(to_char(last_day(to_date('2015-03-24', 'yyyy-MM-dd')), 'WW')) w2
from dual)
select w1+level-1 week from days
connect by level <= w2-w1+1
Subquery days gives minimum and maximum week, query with connect by returns numbers between these values. Please replace example date with :P100_DATE_PICKER.

Get dates in range

I have a start date and an end date. I want to use datepart to check if any of the dates in between fall on weekends. How do I get the dates in between? Since they are not in a field I can't select them.
For this a calendar table would come in handy, but unless you have one already you can generate a series of the dates in between your start and end dates using a number sequence and test the weekday of each date in the series like below.
SET DATEFIRST 1 --set the start of the week to Monday.
DECLARE #startdate date = '2014-10-01', #enddate date = '2014-10-31'
SELECT DATEADD(DAY,number,#startdate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY,number,#startdate) <= #enddate
AND DATEPART(DW,DATEADD(DAY,number,#startdate)) IN (6,7)
This code will list all Saturdays and Sundays in October and the result will be:
2014-10-04
2014-10-05
2014-10-11
2014-10-12
2014-10-18
2014-10-19
2014-10-25
2014-10-26