I'm currently trying to write a stored procedure that can compute the biweekly periods when a date is passed in as a parameter.
The business logic: the first Monday of the year is first day of the biweekly period.
For example in 2010:
period period_start period_end
1 2010-01-04 2010-01-17
2 2010-01-18 2010-01-31
3 2010-02-01 2010-02-14
....
26 2010-12-20 2011-01-02
Passing today's date of 2010-12-31 will return 26, 2010-12-20 and 2011-01-02. How to achieve this?
#Lamak did the hard part, figuring out the first Monday in the year (upvoted). I revised his routine a bit to take a datetime value, like so:
-- Return first Monday for the year being passed in
CREATE FUNCTION dbo.FirstMonday (#TargetDay datetime)
RETURNS DATE
AS
BEGIN
DECLARE #Return DATE
-- Set to first of its year
SET #TargetDay = dateadd(dd, -datepart(dayofyear, #TargetDay) + 1, #TargetDay)
;WITH Dates AS
(
SELECT #TargetDay AS DateVal
UNION ALL
SELECT DATEADD(d, 1, DateVal) AS DateVal
FROM Dates
WHERE DATEADD(d, 1, DateVal) < DATEADD(m, 1, #TargetDay)
)
SELECT #Return = MIN(DateVal)
FROM Dates
WHERE DATENAME(WEEKDAY,DateVal) = 'Monday'
RETURN #Return
END
GO
-- Test it
print dbo.FirstMonday(getdate())
print dbo.FirstMonday('Jan 1, 2010')
From there, it's just some datetime arithmatic:
DECLARE
#FirstMonday datetime
,#TargetDay datetime
,#BiWeek int
SET #TargetDay = getdate()
--SET #TargetDay = 'Jan 17, 2010'
-- Get the first Monday
SET #FirstMonday = dbo.FirstMonday(#TargetDay)
-- Calculate the bi-weekly period
SET #BiWeek = datediff(dd, #FirstMonday, #TargetDay) / 14
-- Print results
PRINT #BiWeek + 1
PRINT dateadd(dd, #BiWeek * 14, #FirstMonday)
PRINT dateadd(dd, #BiWeek * 14 + 13, #FirstMonday)
-- Or return them as a dataset
SELECT
#BiWeek + 1 Period
,dateadd(dd, #BiWeek * 14, #FirstMonday) PeriodStart
,dateadd(dd, #BiWeek * 14 + 13, #FirstMonday) PeriodEnd
This fails when you pick a day in the year that falls before the first Monday, as it returns the first biweekly period after that date. (Or does it fail? You did not specify what the proper period is for such dates... :)
However, as #HLGEM says, depending on what you are doing you are probably better off building and using some form of biweekly period lookup table, especially if you need proper handling of those early dates.
You can create a function to get the first Monday of a year. Something like this should work:
CREATE FUNCTION dbo.FirstMonday(#Year VARCHAR(4))
RETURNS DATE
AS
BEGIN
DECLARE #Return DATE;
WITH Dates AS
(
SELECT CONVERT(DATE,#Year+'0101') AS DateVal
UNION ALL
SELECT DATEADD(d, 1, DateVal) AS DateVal
FROM Dates
WHERE DATEADD(d, 1, DateVal) < DATEADD(m, 1, #Year+'0101')
)
SELECT #Return = MIN(DateVal)
FROM Dates
WHERE DATENAME(WEEKDAY,DateVal) = 'Monday'
RETURN #Return
END
Related
I have the week number and year inside a two variables ($week and $year), and I want to know what is the first day (Monday) of that week, of that year.
For example, the value of my variable ($week) is 30 (ie week 30) and the $year is 2017 (year 2017), I want it to return the first day of week 30 of 2017, which is July 24
How do I do that?
Thank you!
Declare #Year int,#Week int,#YearText varchar(4)
set #Year = 2017
set #Week = 10
set #YearText = #Year
print dateadd(day,1 - datepart(dw, #YearText + '-01-01')+ (#Week-1) * 7,#YearText + '-01-01')
I want to add 5 days to the provided date, but the calculation must skip weekends.
I already know how to add 5 days without skipping weekends:
SELECT DATE_ADD(`date_field`, INTERVAL 5 DAY) As FinalDate
FROM `table_name`;
Now I want the returned value to skip weekends.
Currently if date_field = 2016-07-22 the results will be 2016-07-27
But I want the results to be 2016-07-29
Try this:
SELECT DATE_ADD(
date_field,
INTERVAL 5 +
IF(
(WEEK(date_field) <> WEEK(DATE_ADD(date_field, INTERVAL 5 DAY)))
OR (WEEKDAY(DATE_ADD(date_field, INTERVAL 5 DAY)) IN (5, 6)),
2,
0)
DAY
) AS FinalDate
FROM `table_name`;
How it works:
Firstly, it will add 5 days on your date.
Secondly, when date_field and 5 days later are in two different weeks, it must be added additional 2 days.
Thirdly, when 5 days later is Sat or Sun, it must be added additional 2 days.
This can easily be done for an arbitrary amount of days with a recursive CTE:
WITH RECURSIVE a AS (
SELECT
CURRENT_DATE date, -- Start date
0 days
UNION
SELECT
-- Always increment the date
a.date + INTERVAL 1 DAY AS date,
-- Increment the work day count only if it's not a weekend day
a.days + (WEEKDAY(a.date + INTERVAL 1 DAY) < 5) AS days
FROM a
WHERE
-- Keep going until the week day count reaches 10
a.days < 10 -- Amount of days to add
)
SELECT MAX(date)
FROM a
In the example situation you would use a subquery:
SELECT
(
WITH RECURSIVE a AS (
SELECT
date_field date,
0 days
UNION
SELECT
a.date + INTERVAL 1 DAY AS date,
a.days + (WEEKDAY(a.date + INTERVAL 1 DAY) < 5) AS days
FROM a
WHERE a.days < 5
)
SELECT MAX(date)
FROM a
) AS final_date
FROM table_name
I did try your solution but faced a problem when using it with a larger interval (e.g 20 days). It works perfectly with little intervals though.
Example : for '2017-10-04' + 20 days, your algorithm return '2017-10-26'. It should be '2017-11-01' since we skip 4 weekends.
The numbers of days you add isn't calculated depending on the difference between the 2 week #, so the maximum days you can add is 2 and in my case, it should be 8 (4x2).
I modified your code to end up with this (I also add variables, much more convenient to modify)
SELECT
#ID:='2017-10-04' as initial_date, -- the initial date in the right format to manipulate (add x day)
#DTA:=20 as days_to_add, -- number of days to add
#DA:= DATE_ADD(#ID, INTERVAL #DTA DAY) as date_add,
#LASTDAY := WEEKDAY(#DA) as last_day, -- the day (Monday, Tuesday...) corresponding to the initial date + number of days to add
#WEEK1 := DATE_FORMAT(#ID, '%v') as initial_date_week, -- format the initial date to match week mode 3 (Monday 1-53)
#WEEK2 := DATE_FORMAT(#DA, '%v') as added_date_week_nbr, -- the week # of the initial_date + number of days to add
#WEEKDIFF := #WEEK2 - #WEEK1 as week_difference, -- the difference between week 2 and week 1
DATE_ADD(#ID,
INTERVAL #DTA +
if ( #WEEKDIFF > 0 or #LASTDAY in (5,6),
2,
0
) +
if (#WEEKDIFF > 1,
#WEEKDIFF*2,
0
) DAY
) AS FinalDate
The way I get my week numbers can seems weird but this is because I'm running this in France and my DB seems to be configured in a way that weeks are natively starting by Sunday, "%v" represent the 'mode 3' for weeks, you can check the MySQL documentation here for more details : https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html (ctrl + F > '%v')
I didn't implement the public holiday yet, but I'm thinking of adding X days in the calculation each times one of this day is in the period we're looking at.
According to my (few) tests, this should work. Let me know if not
try this out, should work nicely, basically loop through each of the days and check if they are saturday or sunday, ignore them.
https://social.technet.microsoft.com/wiki/contents/articles/30877.t-sql-extending-dateadd-function-to-skip-weekend-days.aspx
Plus hollydays 1 or 2
select GREATEST(WEEKDAY(NOW()) - 4, 0) 'hollydays'
My solution is to create a function that returns the calculated date, it will consider the consecutive weekends:
DELIMITER $$
CREATE FUNCTION `add_working_days_to_current_month`(ndays INT) RETURNS DATE
NO SQL
BEGIN
declare finalDate, startDate, originalFinalDate DATE;
declare weekNumberStartDate, weekNumberEndDate, weekDiff INT;
set startDate = DATE(CONCAT(YEAR(DATE_SUB(current_date(), INTERVAL 1 MONTH)),"-",MONTH(DATE_SUB(current_date(), INTERVAL 1 MONTH)),"-",DAY(LAST_DAY(DATE_SUB(current_date(), INTERVAL 1 MONTH)))));
set weekNumberStartDate = WEEK(startDate);
set finalDate = DATE_ADD(startDate, INTERVAL ndays DAY);
set originalFinalDate = finalDate;
set weekNumberEndDate = WEEK(finalDate);
IF(weekNumberEndDate != weekNumberStartDate) THEN
set weekDiff = (weekNumberEndDate - weekNumberStartDate) * 2;
set finalDate = DATE_ADD(finalDate, INTERVAL weekDiff DAY);
END IF;
set weekNumberStartDate = WEEK(originalFinalDate);
set weekNumberEndDate = WEEK(finalDate);
IF(weekNumberEndDate != weekNumberStartDate) THEN
set weekDiff = (weekNumberEndDate - weekNumberStartDate) * 2;
set finalDate = DATE_ADD(finalDate, INTERVAL weekDiff DAY);
END IF;
IF(WEEKDAY(finalDate) IN (5, 6)) THEN
set finalDate = DATE_ADD(finalDate, INTERVAL 2 DAY);
END IF;
return finalDate;
END$$
DELIMITER ;
Basically i'm using the same logic as the accepted answer but acumulating the weekends. So for each weekend i will add 2 days and at the end i will if the result date is on weekend i will add 2 more
WHERE datefield BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL 7 DAY
AND WEEKDAY(datefield) NOT IN (5,6);
I working on a financial project where i need to calculate the difference of arrear days. If I use the method datediff() of mysql then it returns the result based on 365 days. I need the result based on 360 days. if use the following sql query
select datediff('20140908','20130908') from dual;
mysql returns the date difference 365. This is the actual date difference but in accounting/Financial calculation the difference is exactly one year (360 days). This is what I want. The result should be 360 instead 365.
Currently I want to use US standard.
to get the same result like Excel, I found the following code to use in MySQL:
select case
when (day(Startdate)>=30 or Startdate=last_day(Startdate) then
case
when(day(Enddate)>=30) then
30*(12*(year(Enddate)-year(Startdate))+month(Enddate)-month(Startdate))
else
30*(12*(year(Enddate)-year(Startdate))+month(Enddate)-month(Startdate))+days(Enddate)-30
end
else
30*(12*(year(Enddate)-year(Startdate))+month(Enddate)-month(Startdate))+days(Enddate)-days(Startdate)
end
Use
TO_DAYS(date2) - To_DAYS(date1)
It returns the number of days between date1 and date2. Then you can do with the result what you need.
sdate = from date
edate = to date
this calculate the days between the 2 date, taking into account that a month is 30 days, 1 year is 360 days, and checking if the date are at the end of the month, so e.g. from 1.1.2019 to 28.2.2019 = 60 days etc.
to be 1 month or 1 year, the edate should be a day before, so 1.1 - 31.1 = 30 days, 1.1 - 1.2 = 31 days
SELECT GREATEST(
0,
(YEAR(DATE_ADD(edate, INTERVAL 1 DAY)) - YEAR(sdate)) * 360 +
(MONTH(DATE_ADD(edate, INTERVAL 1 DAY)) - MONTH(sdate)) * 30 +
(
IF(DAYOFMONTH(DATE_ADD(edate, INTERVAL 1 DAY)) = DAYOFMONTH(LAST_DAY(DATE_ADD(edate, INTERVAL 1 DAY))), 30, DAYOFMONTH(DATE_ADD(edate, INTERVAL 1 DAY))) -
IF(DAYOFMONTH(sdate) = DAYOFMONTH(LAST_DAY(sdate)), 30, DAYOFMONTH(sdate))
)
Pretty late here, but posting my solution for future reference
CREATE FUNCTION `DAYS360_UDF`(sdate DATE, edate DATE)
RETURNS INTEGER
DETERMINISTIC
CONTAINS SQL
BEGIN
DECLARE sdate_360 INTEGER;
DECLARE edate_360 INTEGER;
SET sdate_360 = ( YEAR(sdate) * 360 ) + ( (MONTH(sdate)-1)*30 ) + DAY(sdate);
SET edate_360 = ( YEAR(edate) * 360 ) + ( (MONTH(edate)-1)*30 ) + DAY(edate);
RETURN edate_360 - sdate_360;
END
Usage -
select DAYS360_UDF('20130908', '20140908')
How to get the financial day of the year i.e., if i pass 2nd of april to the function, it should return 2. The financial year starts on 1st of April for every year.
Fiscal calendars are specific to the organization and, although rare, can change. The simplest solution is to create a table that outlines the Fiscal calendar. So, you can mimic this using a CTE but it is better to store it as a table.
With FiscalCalendarStarts As
(
Select 1 As FiscalPeriod
, Cast('20120401' As DateTime) As StartDate
Union All
Select FiscalPeriod + 1
, Case
When Month(StartDate) = 12 Then DateAdd(m, Month(StartDate) - 12 + 1, StartDate)
Else DateAdd(m, 1, StartDate)
End
From FiscalCalendarStarts
Where FiscalPeriod < 12
)
, FiscalCalendar As
(
Select FiscalPeriod
, StartDate
, DateAdd(d, -1, DateAdd(m, 1, StartDate)) As EndDate
From FiscalCalendarStarts
)
Select *
From FiscalCalendar
Where #SomeDate Between StartDate And EndDate
Edit
To get the day count (which I admit I did not provide in the above solution), the trick is to determine the actual fiscal year start date based on the input date. To do that, you could do something like the following, which per your request, I've put into a function
Create Function dbo.FiscalDay ( #Input datetime )
Returns int
As
Begin
Declare #StartDayMonth char(4);
Set #StartDayMonth = '0401';
Return (
Select DateDiff(d, FYStartDate, #Input) + 1
From (
Select DateAdd(yyyy
, Case
When DatePart(dy, #Input) >= DatePart(dy, StartDate) Then 0
Else -1
End
, StartDate) As FYStartDate
From (
Select Cast( Cast(Year(#Input) As char(4))
+ #StartDayMonth As datetime ) As StartDate
) As S1
) As S
)
End
I start with the stub of 0401 which represents the month and day of the start of the fiscal year. To that I prepend the passed date's year so I get something like 20120401 if a date in 2012 was passed. If #Input is later than 1-Apr, then we're in the new fiscal year for the year of the #Input. If #Input is earlier than 1-Apr, then we're in the fiscal year that start on 1-Apr of the previous year. Now that we have the fiscal start date, we can simply find the numbers of days between them and add 1 (otherwise 1-Apr will be seen as day 0 instead of day 1). Note that passing 31-Mar-2012 returns 366 instead of 365 since 2012 was a leap year.
#Olivarsham, The financial year in not common for every country. Some where it is Apr-mar, some where it is Jan-Dec. So It it is your special application requirement then you need to write for your self. I think no standard query for that.
Kindly try this function. This will return your the day number of the fiscal year.
CREATE FUNCTION [dbo].[FiscalDay] (#CurrentDATE datetime)
RETURNS int
AS
BEGIN
DECLARE #FiscalDay int;
DECLARE #YearStartDate DateTime;
Set #YearStartDate=Cast('20120401' As DateTime)
set #FiscalDay = DATEDIFF(DAY,#YearStartDate , #CurrentDATE)
RETURN(#FiscalDay);
END;
GO
I have this code which I'm writing into a stored procedure:
declare #StartTime time
declare #EndTime time
declare #Temp_StartTime time
declare #temp_StartHour int
declare #temp_EndHour int
declare #temp_StartMinute int
declare #temp_EndMinute int
SET #StartTime='22:30:00'
SET #EndTime='00:52:00'
SET #Temp_StartTime=#StartTime
SET #temp_StartHour=DATEPART(HOUR, #StartTime)
SET #temp_EndHour=DATEPART(HOUR, #EndTime)
SET #temp_StartMinute=DATEPART(MI, #StartTime)
SET #temp_EndMinute=DATEPART(MI, #EndTime)
if(#temp_EndMinute>0)
BEGIN
SET #temp_EndHour=#temp_EndHour+1
END
DECLARE #Temp_Table TABLE
(
StartHour int,
StartMinute int,
EndHour int,
EndMinute int,
StartTime time,
EndTime time
)
WHile((#temp_EndHour-#temp_StartHour>=1))
BEGIN
INSERT INTO #Temp_Table
SELECT (DATEPART(HOUR, #Temp_StartTime)) AS StartHour,(DATEPART(MINUTE, #Temp_StartTime)) AS StartMinute,
#temp_StartHour+1 AS EndHour,
0 AS EndMinute, #StartTime as StartTime, #EndTime as EndTime
SET #temp_StartHour=#temp_StartHour+1
SET #Temp_StartTime=DATEADD(HOUR,1,#Temp_StartTime)
if(DATEPART(MI, #Temp_StartTime)!=0)
BEGIN
SET #Temp_StartTime=DATEADD(MI,-#temp_StartMinute,#Temp_StartTime)
END
END
SELECT * FROM #Temp_Table
It works great if you use any time value other than the 00:52:00 example I have up there. For instance, if EndTime was 23:05, the stored procedure works great. I did some research around DATEPART but didn't find anything helpful as to how to get it to calculate midnight at military time properly.
EDIT: When the code runs properly, it calculates the time in how many hours between start and end time and the idea is to store new rows for each hour into the temp table (eventually this is going to be saved to a new table for tracking outages by hour). It works find when I run it with 21:30 to 22:15. I get two rows reflecting 21:00 to 22:00 and 22:00 to 23:00 (this is the logic I want). But throw military midnight in there, and I get no rows returned as the calc won't compute the 00.
I have found examples in my database that show start times of 22:00:0000 and end times of 00:00:00.0000000 and then visa versa. So one way WILL calculate, where start time is 00, but if start time is 21:00:0000 and end time is 00:52:0000 then no dice. I get no rows returned.
If I am not missing anything, this is what you could try instead of your loop:
DECLARE
#StartTime time,
#EndTime time;
SET #StartTime = '22:30:00';
SET #EndTime = '00:52:00';
WITH timerange AS (
SELECT
StartTime = CAST(#StartTime AS datetime),
EndTime = DATEADD(
DAY,
CASE WHEN #StartTime > #EndTime THEN 1 ELSE 0 END,
CAST(#EndTime AS datetime)
)
),
hourly AS (
SELECT
n.number,
t.StartTime,
t.EndTime,
HStart = DATEADD(HOUR, DATEDIFF(HOUR, 0, t.StartTime) + n.number , 0),
HEnd = DATEADD(HOUR, DATEDIFF(HOUR, 0, t.StartTime) + n.number + 1, 0)
FROM timerange t
INNER JOIN master..spt_values n
ON n.number BETWEEN 0 AND DATEDIFF(HOUR, t.StartTime, t.EndTime)
WHERE n.type = 'P'
),
hourly2 AS (
SELECT
number,
HStart = CASE WHEN StartTime > HStart THEN StartTime ELSE HStart END,
HEnd = CASE WHEN EndTime < HEnd THEN EndTime ELSE HEnd END
FROM hourly
)
SELECT
StartHour = DATEPART(HOUR , HStart),
StartMinute = DATEPART(MINUTE, HStart),
EndHour = DATEPART(HOUR , HEnd ),
EndMinute = DATEPART(MINUTE, HEnd ),
StartTime = CAST(HStart AS time),
EndTime = CAST(HEnd AS time)
FROM hourly2
ORDER BY number
;
The output produced is this:
StartHour StartMinute EndHour EndMinute StartTime EndTime
----------- ----------- ----------- ----------- ---------------- ----------------
22 30 23 0 22:30:00.0000000 23:00:00.0000000
23 0 0 0 23:00:00.0000000 00:00:00.0000000
0 0 0 52 00:00:00.0000000 00:52:00.0000000