I need to calculate the next business day (business days being Monday - Friday) in Oracle. At the moment in our reporting program we use a variable it supports <|Tomorrow|> but the problem is that customers do not receive the require details for say a Monday's delivery until a Sunday which is pointless since no one is there.
So we need to calculate Monday on the previous Friday, Tuesday on Monday, Wednesday on Tuesday, Thursday on Wednesday and Friday on Thursday.
What is the best method to do it within directly Oracle since our reporting program does not seem to have a next business day variable. Basically we need to change Where ORDER_HEADER.DELIVERY_DATE = '<|Tomorrow|>' to Where ORDER_HEADER.DELIVERY_DATE = ** next business day **
This is Oracle 10g.
Select ORDER_HEADER.DELIVERY_DATE As "Delivery Date",
ORDER_HEADER.ORDER_NO As "Document No",
ORDER_HEADER.CUSTOMER_ORDER_NO As "Customer Order No",
ORDER_TOTALS.ORDER_TOTAL_QUANTITY As "Total Items",
ORDER_TOTALS.ORDER_TOTAL_NET As "Total Net"
From ORDER_HEADER
Inner Join ORDER_TOTALS On ORDER_HEADER.ORDER_NO = ORDER_TOTALS.ORDER_NO
Where ORDER_HEADER.DELIVERY_DATE = '<|Tomorrow|>' And ORDER_HEADER.CUSTOMER_NO = :Param1
Order By "Document No"
These things are normally done with a table that lists all the working days in a year rather than a naive formula like:
CASE WHEN (1 + TRUNC (SYSDATE) - TRUNC (SYSDATE, 'IW')) < 5
THEN 1 + (1 + TRUNC (SYSDATE) - TRUNC (SYSDATE, 'IW'))
ELSE 1
END --(on monday to thursday this returns 2 to 5, on fri/sat/sun this returns 1)
CASE WHEN (1 + TRUNC (SYSDATE) - TRUNC (SYSDATE, 'IW')) < 5
THEN TRUNC (SYSDATE) + 1
ELSE TRUNC (SYSDATE + 4), 'IW')
END --(on monday to thursday this returns tomorrow's date, on fri/sat/sun it returns next monday's date)
Using a table allows you to factor in things like bank holidays, national holidays, religious festivals, voting days, mourning days, ad hoc days off that may occur for company, or political reasons etc etc. Maintaining the table then becomes an important task. Retrieving the next working day is essentially a case of querying the table for the MIN(date) WHERE date > current_date
A typical method would be:
WHERE ( (to_char(sysdate, 'dy') in ('sun', 'mon', 'tue', 'wed', 'thu') and oh.DELIVERY_DATE = TRUNC(SYSDATE) + 1
) or
(to_char(sysdate, 'dy') in ('sat') and oh.DELIVERY_DATE = TRUNC(SYSDATE) + 2
) or
(to_char(sysdate, 'dy') in ('fri') and oh.DELIVERY_DATE = TRUNC(SYSDATE) + 3
)
)
I strongly agree with the other answer that you should have a calendar table for your business. The next business day is not only about weekends but also about holidays.
If you looking for only week days, then one way is create your own function which returns next business day:
create or replace function calc_date(dt date)
return date as ret date;
begin
SELECT min(dt) into ret from (
SELECT ( dt + 1 ) AS dt FROM DUAL
UNION
SELECT ( dt + 2 ) AS dt FROM DUAL
UNION
SELECT ( dt + 3 ) AS dt FROM DUAL
) t
where TO_CHAR(dt, 'D') not in (1,7);
return ret;
end;
Call
select calc_date('2017-09-08') from dual;
Related
So I've been trying to fetch some daily data with SYSDATE on a date type YYYYMMDD as following:
SELECT dates, trunc(calendar_date, 'DD') calendar_dates, weekday_nbr
FROM db.date
WHERE dates BETWEEN to_char(TRUNC(SYSDATE)-2, 'YYYYMMDD') AND to_char(TRUNC(SYSDATE)-1, 'YYYYMMDD')
But now I'm trying to use the same but on a YYYY+MM+Week date type with not much success
I tried using:
SELECT T time, period, fiscal_week
FROM db.time
WHERE time BETWEEN to_char(TRUNC(SYSDATE)-2, 'W') AND to_char(TRUNC(SYSDATE)-1,'W')
With time as a 7 digit number, and period and fiscal week as a 2 digit number
Knowing that there's no way I can truncate such date type, how can TRUNC SYSDATE YYYY+MM+Week to get the data on the last 2 weeks?
Also I was thinking about maybe getting the totals from a set day and then dropping all but the last 2 weeks, but on the long run maybe that would be time consuming.
Knowing that there's no way I can truncate such date type, how can TRUNC SYSDATE YYYY+MM+Week to get the data on the last 2 weeks?
Assuming that your fiscal weeks are from Monday-Sunday then you can truncate to the start of the ISO week (which is always Midnight on Monday) and use that for the basis of the comparison:
SELECT *
FROM db.time
WHERE dates >= TRUNC(SYSDATE, 'IW') - INTERVAL '14' DAY
AND dates < TRUNC(SYSDATE, 'IW')
If you have a column that is for weeks then you should still use a DATE data type and add a CHECK constraint (and can use virtual columns to generate the week and the year):
CREATE TABLE time (
dt DATE
CHECK (dt = TRUNC(dt, 'IW')),
year NUMBER(4,0)
GENERATED ALWAYS AS (EXTRACT(YEAR FROM dt)),
month NUMBER(2,0)
GENERATED ALWAYS AS (EXTRACT(MONTH FROM dt)),
week NUMBER(1,0)
GENERATED ALWAYS AS (FLOOR((dt - TRUNC(dt, 'MM'))/7) + 1),
time VARCHAR2(7)
GENERATED ALWAYS AS (
CAST(
TO_CHAR(dt, 'YYYYMM')
|| (FLOOR((dt - TRUNC(dt, 'MM'))/7) + 1)
AS VARCHAR2(7)
)
)
-- ...
);
fiddle
Then you can use the logic above on the date column.
If you do not have a DATE column then you will need to convert your YYYYMMW number into a DATE and then use the logic above.
For example, if the logic for your fiscal weeks (which you have not described) is that the first week of each month starts on the first Monday of the month then you can convert the YYYYMMW number to a DATE using:
SELECT NEXT_DAY(
TO_DATE(SUBSTR(time, 1, 6), 'YYYYMM') - INTERVAL '1' DAY,
'MONDAY'
) + INTERVAL '7' DAY * (SUBSTR(time, 7, 1) - 1) AS week_start
FROM db.time
and then could use it to filter the table using:
SELECT *
FROM (
SELECT t.*,
NEXT_DAY(
TO_DATE(SUBSTR(time, 1, 6), 'YYYYMM') - INTERVAL '1' DAY,
'MONDAY'
) + INTERVAL '7' DAY * (SUBSTR(time, 7, 1) - 1) AS week_start
FROM db.time t
)
WHERE week_start >= TRUNC(SYSDATE, 'IW') - INTERVAL '14' DAY
AND week_start < TRUNC(SYSDATE, 'IW')
If you have different logic for calculating when fiscal weeks start then you will need to apply that logic to the conversion.
I would like to insert in table :
the first day of the week starting from Monday.
the last day of the week as Sunday.
the WEEK number => (1-52 or 1-53) based on the ISO standard.
First i tried to select first day, the last day and week number for specific date and it's works :
WITH ranges AS
(
SELECT to_date('29-10-2012', 'dd-MM-yyyy') AS DATE_TEST FROM DUAL
)
SELECT DATE_TEST "DATE",
TO_CHAR( NEXT_DAY( TRUNC(DATE_TEST) , 'SUNDAY' )) "WEEK END DATE",
TO_CHAR(TO_DATE(DATE_TEST,'DD-MON-YYYY'),'WW')+1 "WEEK NUMBER"
FROM ranges ;
But now i would like to display this data between two dates, but i get result only for the start_date. someone can help please.
after, when all good i will insert all in the table.
Thanks
WITH ranges AS(
select to_date('29-OCT-2012', 'dd-MM-yyyy') START_DATE,
to_date('31-DEC-2016', 'dd-MM-yyyy') END_DATE
from dual
)
SELECT START_DATE "DATE",
TO_CHAR( NEXT_DAY( TRUNC(START_DATE) , 'SUNDAY' )) "WEEK END DATE",
TO_CHAR(TO_DATE(START_DATE,'DD-MON-YYYY'),'WW')+1 "WEEK NUMBER"
FROM ranges ;
Format WW returns Week of year (1-53) where week 1 starts on the first day of the year and continues to the seventh day of the year, see Datetime Format Elements
In order to get the week number according to ISO-8601 standard use format IW. I would suggest like this:
WITH ranges AS(
SELECT
DATE '2012-10-29' START_DATE,
DATE '2016-12-31' END_DATE
FROM dual
)
SELECT
START_DATE, END_DATE,
TRUNC(START_DATE + 7*(LEVEL-1), 'IW') AS Week_Start_Date,
TRUNC(START_DATE + 7*(LEVEL-1), 'IW') + 6 AS Week_End_Date,
TO_CHAR(TRUNC(START_DATE + 7*(LEVEL-1)), 'IYYY-"W"IW') WEEK_NUMBER
FROM ranges
CONNECT BY START_DATE + 7*(LEVEL-1) <= END_DATE;
Looks like you're looking for a calendar.
Based on your RANGES CTE, there's another - calendar which utilizes hierarchical query to create all dates between start_date and end_date. Once you have all dates, extract values you're interested in.
SQL> with
2 ranges as
3 (select to_date('29-OCT-2012', 'dd-MM-yyyy') start_date,
4 to_date('31-DEC-2016', 'dd-MM-yyyy') end_date
5 from dual
6 ),
7 calendar as
8 (select start_date + level - 1 as datum
9 from ranges
10 connect by level <= end_date - start_date + 1
11 )
12 select
13 min(datum) start_date,
14 min(next_day(datum, 'sunday')) week_end_date,
15 to_char(datum, 'ww') week_number
16 from calendar
17 group by to_char(datum, 'yyyy'), to_char(datum, 'ww')
18 order by 1;
START_DATE WEEK_END_D WE
---------- ---------- --
29-10-2012 04-11-2012 44
04-11-2012 11-11-2012 45
11-11-2012 18-11-2012 46
18-11-2012 25-11-2012 47
25-11-2012 02-12-2012 48
<snip>
09-12-2016 11-12-2016 50
16-12-2016 18-12-2016 51
23-12-2016 25-12-2016 52
30-12-2016 01-01-2017 53
222 rows selected.
SQL>
How are you?
to help, I tried to do some research, I found these links.
enter link description here
enter link description here
I found:
SELECT
ROUND((TRUNC(SYSDATE) - TRUNC(SYSDATE, 'YEAR')) / 7,0) CANTWEEK,
NEXT_DAY(SYSDATE, 'SUNDAY') - 7 FIRSTDAY,
NEXT_DAY(SYSDATE, 'SUNDAY') - 1 LASTDAY
FROM DUAL
and
select
to_char(sysdate - to_char(sysdate, 'd') + 2, 'yyyymmdd') first_day_of_week
, to_char(sysdate - to_char(sysdate, 'd') + 8, 'yyyymmdd') last_day_of_week
from
dual
and
select sysdate AS today,
TRUNC(next_day(sysdate,'MONDAY')-8) as DOMINGO,
TRUNC(next_day(sysdate,'SATURDAY')) as SABADO
from dual
I don't have oracle here so I couldn't test it well but it should solve what you need, anything let me know :)
Here is a way to make the common table expression return a range of dates.
with ranges (dt) as (
select to_date('29-OCT-2012', 'dd-MM-yyyy') as dt
from dual
union all
select ranges.dt+1
from ranges
where ranges.dt < to_date('31-DEC-2016', 'dd-MM-yyyy')
)
Then you can use that to calculate the other values.
SELECT dt "DATE"
, TO_CHAR(NEXT_DAY(TRUNC(dt), 'SUNDAY')) "WEEK END DATE"
, TO_CHAR(TO_DATE(dt,'DD-MON-YYYY'),'WW') + 1 "WEEK NUMBER"
, cast(TO_CHAR(dt, 'WW') as int) +
case when cast(TO_CHAR(dt, 'D') as int) < cast(TO_CHAR(trunc(dt, 'year'), 'D') as int) then 1 else 0 end WeekNumberInYear
FROM ranges ;
If you want to have all of your date calculations done at once, check out an example here: https://dbfiddle.uk/?rdbms=oracle_18&fiddle=35e407af3b5bf711bb7ae53b8cf0e608
I got this query on order to get all the days from the first day of the year (01/01/2018) to the end of next year (31/12/2019).
SELECT MYDATE,
TO_CHAR(NR_OF_SUNDAYS + 1,'FM09') WEEK_NUM,
FROM
(
SELECT MYDATE,
( (TRUNC(MYDATE,'DAY') - TRUNC(TRUNC(MYDATE,'YYYY'),'DAY')) / 7 ) +
CASE WHEN TO_CHAR(TRUNC(MYDATE,'YYYY'),'DAY') = 'SUN' THEN 1 ELSE 0 END AS NR_OF_SUNDAYS
FROM
( SELECT TRUNC (SYSDATE, 'YY') - 1 + LEVEL AS MYDATE
FROM DUAL
CONNECT BY LEVEL <= TRUNC (ADD_MONTHS (SYSDATE, 24), 'YY') -
TRUNC (SYSDATE, 'YY')
)
)
I need a column that specifies the following cases:
1) CASE WHEN MYDATE < TO_CHAR(SYSDATE, 'DD/MM/YYYY') THEN 'PAST DUE'
(this works its easy and no problem)
2) if my current =< mydate
week_num then 'CURRENT WEEK'(Excluding PAST DUE)
3) if my current week + one week then
'NEXT WEEK' (Excluding PAST DUE)
4) else FUTURE
Thanks a lot for your help.
So, in my answer I tried retain the logic behind your week number calculation.
However keep in mind that you could calculate week number using oracle to_char(date,'WW'), to_char(date,'IW'), to_char(date,'W') functions and then your life would be easier.
WW Week of year (1-53) where week 1 starts on the first day of the year and continues to the seventh day of the year.
W Week of month (1-5) where week 1 starts on the first day of the month and ends on the seventh.
IW Week of year (1-52 or 1-53) based on the ISO standard.
Having said all that here is my solution that uses only sql (note that defining and using a function would be a lot easier), based on your calculation method.
with date_table as (
SELECT MYDATE, to_number(TO_CHAR(NR_OF_SUNDAYS + 1,'FM09')) WEEK_NUM, to_number(to_char(MYDATE+1,'IW')) as nu
FROM
(
SELECT MYDATE,
( (TRUNC(MYDATE,'DAY') - TRUNC(TRUNC(MYDATE,'YYYY'),'DAY')) / 7 ) +
CASE WHEN TO_CHAR(TRUNC(MYDATE,'YYYY'),'DY', 'NLS_DATE_LANGUAGE = american') = 'SUN' THEN 1 ELSE 0 END AS NR_OF_SUNDAYS
FROM
( SELECT TRUNC (SYSDATE, 'YY') - 1 + LEVEL AS MYDATE
FROM DUAL
CONNECT BY LEVEL <= TRUNC (ADD_MONTHS (SYSDATE, 24), 'YY') -TRUNC (SYSDATE,'YY')
)
)
),
todays_week as
(
select distinct WEEK_NUM from date_table
where trunc(sysdate)=trunc(mydate)
),
pre_final as (
select MYDATE,WEEK_NUM, (select WEEK_NUM from todays_week) as todaysweek from date_table)
select MYDATE,sysdate,WEEK_NUM,todaysweek,
case when trunc(MYDATE) < trunc(sysdate) then 'PAST DUE'
when todaysweek = WEEK_NUM and abs(MYDATE-sysdate)<=7 then 'CURRENT WEEK'
when todaysweek +1 = WEEK_NUM and abs(MYDATE-sysdate)<=14 then 'Next Week'
else 'Future' end as description
from pre_final;
The main idea is to find today's week number and then use case when.
Here is my fiddle link with the results.
http://sqlfiddle.com/#!4/3149e4/148
EDIT 1:
Now, similar results one could achive with something like this:
select res.*,
case when trunc(MYDATE) < trunc(sysdate) then 'PAST DUE'
when todaysweek = WEEK_NUM and abs(MYDATE-sysdate)<=7 then 'CURRENT WEEK'
when todaysweek +1 = WEEK_NUM and abs(MYDATE-sysdate)<=14 then 'Next Week'
else 'Future' end as description
from (
SELECT MYDATE, to_number(to_char(MYDATE,'IW')) as WEEK_NUM,to_number(to_char(sysdate,'IW')) as todaysweek
FROM
( SELECT TRUNC (SYSDATE, 'YY') - 1 + LEVEL AS MYDATE
FROM DUAL
CONNECT BY LEVEL <= TRUNC (ADD_MONTHS (SYSDATE, 24), 'YY') -TRUNC (SYSDATE,'YY')
)) res
Suppose I enter WeekNo: 14 the query should return the From Date: April 4th 2016, since the week 14 starts from 4th April to 10th April
select to_date('14','iw') FROM dual;
something like this ? (it work for current year) there discard data from another years
with dates as (select to_char(
to_date('1.01.'||extract(year from sysdate),'dd.mm.yyyy' ) + level -1
,'IW') we,
to_date('1.01.'||extract(year from sysdate),'dd.mm.yyyy' ) + level -1 da
from dual
connect by level <= 365 + 10 )
select * from (
select case
when -- we = null if number of week in jan > 1,2,3,4....
((to_number(we) > 40 )
and extract(year from sysdate) = extract(year from da)
and extract(month from da) = '01') or
-- we = null when current year < year of da
(extract(year from sysdate) != extract(year from da))
then
null
else we
end we,
da
from dates
)
where we = 14
and rownum = 1
Dealing with ISO-Weeks is not trivial, for example January, 1st 2016 is week 53 of 2015, see
select to_char(date '2016-01-01', 'iyyy-"W"iw') from dual;
So, providing only the week number without the (ISO-) year is ambiguous - although it is obvious as along as you are not around new-years date.
Some time ago I wrote this function to get the date from ISO-Week.
FUNCTION ISOWeekDate(week INTEGER, YEAR INTEGER) RETURN DATE DETERMINISTIC IS
res DATE;
BEGIN
IF week > 53 OR week < 1 THEN
RAISE VALUE_ERROR;
END IF;
res := NEXT_DAY(TO_DATE( YEAR || '0104', 'YYYYMMDD' ) - 7, 'MONDAY') + ( week - 1 ) * 7;
IF TO_CHAR(res, 'fmIYYY') = YEAR THEN
RETURN res;
ELSE
RAISE VALUE_ERROR;
END IF;
END ISOWeekDate;
Of course you can just select NEXT_DAY(TO_DATE( YEAR || '0104', 'YYYYMMDD' ) - 7, 'MONDAY') + ( week - 1 ) * 7;, however this would not be error-safe if somebody uses the wrong year.
If it is not an issue to append the year to the week you are looking for, you can also use this :
SELECT (TRUNC ( TO_DATE (SUBSTR ('201627', 1, 4) || '0131', 'YYYY'|| 'MMDD'), 'IYYY')
+ ( 7 * ( TO_NUMBER (SUBSTR ('201627', 5)) - 1)) ) AS iw_Monday
FROM dual
With in this example 201627 being the YYYYIW you are looking for.
It will return the date of the MONDAY of that week.
Found it on Oracle forums, there are a couple of other solutions there. I found this one to be the most elegant.
The advantage is that you do everything from the SELECT, and you don't need either a function or PL/SQL or a WHERE clause.
Disadvantages : you must append the year and specify your search week 2 times, unless you use a variable
Here is a simple and direct computation, taking advantage of various Oracle date functions. Since it compares to what Oracle already counts as ISO week etc., it shouldn't be subject to any of the difficulties other solutions correctly point to and address with additional code.
The "magic number" 14 in the formula should instead be a bind variable, perhaps :iw, or some other mechanism of inputting the ISO week number into the query.
select trunc(sysdate, 'iw') - 7 * (to_number(to_char(trunc(sysdate), 'iw')) - 14) as dt
from dual;
DT
----------
2016-04-04
1 row selected.
How do I find the number of fridays between two dates(including both the dates) using a select statement in oracle sql?
This will do it:
select ((next_day(date2-7,'FRI')-next_day(date-1,'FRI'))/7)+1 as num_fridays
from data
Perhaps best if I break that down. The NEXT_DAY function returns the next day that is a (Friday in this case) after the date.
So to find the first Friday after d1 would be:
next_day( d1, 'FRI')
But if d1 is a Friday that would return the following Friday, so we adjust:
next_day( d1-1, 'FRI')
Similarly to find the last Friday up to and including d2 we do:
next_day( d1-7, 'FRI')
Subtracting the 2 gives a number of days: 0 if they are the same date, 7 if they a re a week apart and so on:
next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')
Convert to weeks:
(next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')) / 7
Finally, if they are the same date we get 0, but really there is 1 Friday, and so on so we add one:
((next_day( d1-7, 'FRI') - next_day( d1-1, 'FRI')) / 7) + 1
I have to throw in my two cents for using a calendar table. (It's a compulsion.)
select count(*) as num_fridays
from calendar
where day_of_week = 'Fri'
and cal_date between '2011-01-01' and '2011-02-17';
num_fridays
-----------
6
Dead simple to understand. Takes advantage of indexes.
Maybe I should start a 12-step group. Calendar Table Anonymous.
See:
Why should I consider using an auxiliary calendar table?
The article's code is specifically for SQL Server but the techniques are portable to most SQL platforms.
With a Calendar table in place your query could be as simple as
SELECT COUNT(*) AS friday_tally
FROM YourTable AS T1
INNER JOIN Calendar AS C1
ON C1.dt BETWEEN T1.start_date AND T1.end_date
WHERE C1.day_name = 'Friday'; -- could be a numeric code
select sum(case when trim(to_char(to_date('2009-01-01','YYYY-MM-DD')+rownum,'Day')) = 'Friday' then 1 else 0 end) number_of_fridays
from dual
connect by level <= to_date('&end_date','YYYY-MM-DD') - to_date('&start_date','YYYY-MM-DD')+1;
Original source - http://forums.oracle.com/forums/thread.jspa?messageID=3987357&tstart=0
Try modifying this one:
CREATE OR REPLACE FUNCTION F_WORKINGS_DAYS
(V_START_DATE IN DATE, V_END_DATE IN DATE)
RETURN NUMBER IS
DAY_COUNT NUMBER := 0;
CURR_DATE DATE;
BEGIN -- loop through and update
CURR_DATE := V_START_DATE;
WHILE CURR_DATE <= V_END_DATE
LOOP
IF TO_CHAR(CURR_DATE,'DY') NOT IN ('SAT','SUN') -- Change this bit to ignore all but Fridays
THEN DAY_COUNT := DAY_COUNT + 1;
END IF;
CURR_DATE := CURR_DATE + 1;
END LOOP;
RETURN DAY_COUNT;
END F_WORKINGS_DAYS;
/
SELECT (NEXT_DAY('31-MAY-2012','SUN')
-NEXT_DAY('04-MAR-2012','SUN'))/7 FROM DUAL
select ((DATEDIFF(dd,#a,#b)) + DATEPART(dw,(#a-6)))/7