Business days are Monday through Friday.
Given I have a datetime field scheduled_for, how can I find the next business date and return that in a column alias?
I've tried something from another SO answer but it doesn't work as intended.
EXTRACT(ISODOW FROM v.scheduled_for)::integer) % 7 as next_business_day,
Error:
Query 1 ERROR: ERROR: syntax error at or near ")"
LINE 3: EXTRACT(ISODOW FROM v.scheduled_for)::integer % 7) as next...
^
Edit:
Thanks for the suggestions, I've attempted this:
SELECT
v.id AS visit_id,
(IF extract(''dow'' from v.scheduled_for) = 0 THEN
return v.scheduled_for + 1::integer;
ELSIF extract(''dow'' from v.scheduled_for) = 6 THEN
return v.scheduled_for - 1::integer;
ELSE
return v.scheduled_for;
) as next_business_day,
'' as invoice_ref_code,
The error I get is:
Query 1 ERROR: ERROR: syntax error at or near ")"
LINE 1: ) as next_business_day,
^
To generalize you need to create a function to calculate the next business day from a given date.
create or replace function utl_next_business_day(date_in date default current_date)
returns date
language sql immutable leakproof strict
as $$
with cd as (select extract(isodow from date_in)::integer d)
select case when d between 1 and 4
then date_in + 1
else date_in + 1 + (7-d)
end
from cd;
$$;
--- any single date
select current_date, utl_next_business_day();
-- over time span (short)
select gdate::date for_date, utl_next_business_day(gdate::date) next_business_day
from generate_series( current_date, current_date + 14, interval '1 day') gdate;
-- around year end over a time span
with test_date (dt) as
( values (date '2019-12-31')
, (date '2020-12-31'), (date '2021-12-31'),(date '2022-12-31')
, (date '2021-01-01'), (date '2022-01-01'),(date '2023-01-01')
)
select dt, utl_next_business_day(dt) from test_date
order by dt;
Alternatively with the calendar table suggestion from #Eric we get.
-- create and populate work table
create table bus_day_calendar ( bus_day date);
insert into bus_day_calendar (bus_day)
select utl_next_business_day(gdate::date)
from generate_series( date '2018-12-31', date '2023-01-01', interval '1 day') gdate
where extract(isodow from gdate)::integer not in (6,7) ;
--- Function to return next business day
create or replace function utl_next_cal_business_day(date_in date default current_date)
returns date
language sql stable leakproof strict
as $$
select min(bus_day)
from bus_day_calendar
where bus_day > date_in;
$$;
--- any single date
select current_date, utl_next_cal_business_day();
-- over time span (short)
select gdate::date for_date, utl_next_cal_business_day(gdate::date) next_business_day
from generate_series( current_date, current_date + 14, interval '1 day') gdate;
-- around year end over a time span
with test_date (dt) as
( values (date '2019-12-31')
, (date '2020-12-31'), (date '2021-12-31'),(date '2022-12-31')
, (date '2021-01-01'), (date '2022-01-01'),(date '2023-01-01')
)
select dt, utl_next_cal_business_day(dt) from test_date
order by dt;
Neither of these as they currently stand handle a non-business day that falls on Mon-Fri, but both can be modified to do so. Since the calendar table requires only deleting roes I think that becomes the superior method if this is necessary.
Related
I need to calculate the number of days between two dates as decimal, excluding the weekends and holidays by using a custom function in Oracle SQL.
There are similar questions on the site; however as I could see, none of them asks for an output as decimal using a custom function. The reason why I need a decimal is to be able to use/extract time component afterwards. If there is already a question like this, please just share the link.
Tried to write a function as below with the help of the additional content I found on the internet thanks to the author. The inner subqueries works fine seperately, but it doesn't work as a whole function.
In brief, the idea is:
(calculate the day difference between startdate and enddate) -> (exclude the number of weekend days between startdate and enddate) -> (exclude the number of weekends between startdate and enddate)
When I try to save the function, it gives the error PLS-00103: Encountered the symbol "end-of-file". Since I am already new in functions maybe missing something basic.
Lastly, also please let me know if you have suggestions on how to make the code more efficient.
Thanks in advance!
CREATE OR REPLACE FUNCTION NET_WORKING_DAYS (startdate IN DATE, enddate IN DATE)
RETURN NUMBER IS
WORKINGDAYS_BETWEEN NUMBER;
BEGIN
SELECT
-- number of days between startdate and enddate
(
SELECT (TO_DATE('20160831150000','YYYYMMDDHH24MISS') - TO_DATE('20160801000000','YYYYMMDDHH24MISS') ) DAYS_BETWEEN
FROM DUAL
)
-
-- number of weekend days (after a given date)
(
SELECT COUNT(1) WEEKEND_DAYS_BETWEEN
FROM
(
SELECT
TO_DATE('20160701000000','YYYYMMDDHH24MISS') + SEQ as day_date, --2016/07/01 is a constant/given date for this formula
TO_CHAR(TO_DATE('20160701000000','YYYYMMDDHH24MISS') + SEQ , 'D') day_of_week
FROM
(
SELECT ROWNUM-1 SEQ
FROM ( SELECT 1 FROM DUAL CONNECT BY LEVEL<= 365 * 5) --5 years
)
ORDER BY 1
)
WHERE day_of_week IN (6,7)
AND day_date > TO_DATE('20160801000000','YYYYMMDDHH24MISS') --this should be replaced with startdate parameter
AND day_date < TO_DATE('20160831000000','YYYYMMDDHH24MISS') --this should be replaced with enddate parameter
)
-
-- number of holidays (after a given date)
(
SELECT COUNT(1)
FROM HOLIDAYS
WHERE HOLIDAY_DATE > TO_DATE('20160801000000','YYYYMMDDHH24MISS') --this should be replaced with startdate parameter
AND HOLIDAY_DATE < TO_DATE('20160831000000','YYYYMMDDHH24MISS') --this should be replaced with enddate parameter
)
INTO WORKINGDAYS_BETWEEN
FROM DUAL;
RETURN WORKINGDAYS_BETWEEN;
END NET_WORKING_DAYS;
**EDIT-1: Holidays are already defined in HOLIDAYS table in the database and for this date range from 20160801000000 to 20160831000000 , 30.06.2016 is the holiday date.
You do not need to use a row generator to enumerate every day to get the number of week days - it can be done using a simple calculation:
From my answer here:
CREATE FUNCTION getWorkingDays (
in_start_date IN DATE,
in_end_date IN DATE
) RETURN NUMBER DETERMINISTIC
IS
p_start_date DATE;
p_end_date DATE;
p_working_days NUMBER;
p_holiday_days NUMBER;
BEGIN
IF in_start_date IS NULL OR in_end_date IS NULL THEN
RETURN NUll;
END IF;
p_start_date := LEAST( in_start_date, in_end_date );
p_end_date := GREATEST( in_start_date, in_end_date );
-- 5/7 * ( Number of days between monday of the week containing the start date
-- and monday of the week containing the end date )
-- + LEAST( day of week for end date, 5 )
-- - LEAST( day of week for start date, 5 )
p_working_days := ( TRUNC( p_end_date, 'IW' ) - TRUNC( p_start_date, 'IW' ) ) * 5 / 7
+ LEAST( p_end_date - TRUNC( p_end_date, 'IW' ), 5 )
- LEAST( p_start_date - TRUNC( p_start_date, 'IW' ), 5 );
SELECT COALESCE(
SUM(
LEAST( p_end_date, holiday_date + INTERVAL '1' DAY )
- GREATEST( p_start_date, holiday_date )
),
0
)
INTO p_holiday_days
FROM Holidays
WHERE HOLIDAY_DATE BETWEEN TRUNC( p_start_date )
AND TRUNC( p_end_date )
AND HOLIDAY_DATE - TRUNC( HOLIDAY_DATE, 'IW' ) < 5;
RETURN GREATEST( p_working_days - p_holiday_days, 0 );
END;
/
As my title, I have the following code:
SELECT
*
FROM
de.Department
WHERE
de.flag = 1
AND de.DepartmentNum IN (10,4)
AND de.status IN (0,-1,100)
AND datediff('dd',de.datequit,'30-SEP-19') > 9
The datediff function make my query run very slow(16s for 11 records), and cost also very high(~43k).
Here is my datediff function code
create or replace FUNCTION DATEDIFF
(
P_TYPE_DATE IN VARCHAR2
, P_START_DATE IN TIMESTAMP
, P_END_DATE IN TIMESTAMP
) RETURN NUMBER AS
v_Result NUMBER := -1;
BEGIN
IF P_TYPE_DATE IS NOT NULL AND P_START_DATE IS NOT NULL AND P_END_DATE IS NOT NULL THEN
CASE UPPER(P_TYPE_DATE)
WHEN 'DD' THEN RETURN ROUND(TRUNC(P_END_DATE,'DD') - TRUNC(P_START_DATE,'DD'),0);
WHEN 'HH' THEN RETURN ROUND((TRUNC(P_END_DATE,'HH') - TRUNC(P_START_DATE,'HH')) * 24,0);
WHEN 'MI' THEN RETURN ROUND((TRUNC(P_END_DATE,'MI') - TRUNC(P_START_DATE,'MI')) * 24 * 60,0);
WHEN 'SS' THEN RETURN ROUND((TRUNC(P_END_DATE,'MI') - TRUNC(P_START_DATE,'MI')) * 24 * 60 * 60 + extract(second from (P_END_DATE - P_START_DATE)),0);
ELSE RETURN NULL;
END CASE;
END IF;
RETURN NULL;
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20001,'An error was encountered - '||SQLCODE||' -ERROR- '||SQLERRM);
END DATEDIFF;
I used SELECT * because I want to get almost column in Department table, so it no more change if I SELECT some columns which I need.
Can I re-write to improve performance and cost?
Mayny thanks!
I created a function named datediff as a datediff function in SQL, sir
Don't use custom functions as they prevent Oracle from using an index on the column; instead just compare the column to the static values:
SELECT *
FROM Department
WHERE flag = 1
AND DepartmentNum IN (10,4)
AND status IN (0,-1,100)
AND datequit > DATE '2019-09-30' + INTERVAL '9' DAY
or
AND datequit > DATE '2019-09-30' + NUMTODSINTERVAL( 9, 'DAY' )
or
AND datequit > DATE '2019-09-30' + 9
Here is my datediff function code
...
WHEN 'DD' THEN RETURN ROUND(TRUNC(P_END_DATE,'DD') - TRUNC(P_START_DATE,'DD'),0);
...
If you want to do an equivalent comparison to using TRUNC to ignore the time components then change from using greater-than comparison to using greater-than-or-equal-to and add one time unit (day in your example) to the expected difference. For example:
SELECT *
FROM Department
WHERE flag = 1
AND DepartmentNum IN (10,4)
AND status IN (0,-1,100)
AND datequit >= DATE '2019-09-30' + INTERVAL '10' DAY
I need to calculate the number of days between two dates as decimal, excluding the weekends and holidays by using a custom function in Oracle SQL.
There are similar questions on the site; however as I could see, none of them asks for an output as decimal using a custom function. The reason why I need a decimal is to be able to use/extract time component afterwards. If there is already a question like this, please just share the link.
Tried to write a function as below with the help of the additional content I found on the internet thanks to the author. The inner subqueries works fine seperately, but it doesn't work as a whole function.
In brief, the idea is:
(calculate the day difference between startdate and enddate) -> (exclude the number of weekend days between startdate and enddate) -> (exclude the number of weekends between startdate and enddate)
When I try to save the function, it gives the error PLS-00103: Encountered the symbol "end-of-file". Since I am already new in functions maybe missing something basic.
Lastly, also please let me know if you have suggestions on how to make the code more efficient.
Thanks in advance!
CREATE OR REPLACE FUNCTION NET_WORKING_DAYS (startdate IN DATE, enddate IN DATE)
RETURN NUMBER IS
WORKINGDAYS_BETWEEN NUMBER;
BEGIN
SELECT
-- number of days between startdate and enddate
(
SELECT (TO_DATE('20160831150000','YYYYMMDDHH24MISS') - TO_DATE('20160801000000','YYYYMMDDHH24MISS') ) DAYS_BETWEEN
FROM DUAL
)
-
-- number of weekend days (after a given date)
(
SELECT COUNT(1) WEEKEND_DAYS_BETWEEN
FROM
(
SELECT
TO_DATE('20160701000000','YYYYMMDDHH24MISS') + SEQ as day_date, --2016/07/01 is a constant/given date for this formula
TO_CHAR(TO_DATE('20160701000000','YYYYMMDDHH24MISS') + SEQ , 'D') day_of_week
FROM
(
SELECT ROWNUM-1 SEQ
FROM ( SELECT 1 FROM DUAL CONNECT BY LEVEL<= 365 * 5) --5 years
)
ORDER BY 1
)
WHERE day_of_week IN (6,7)
AND day_date > TO_DATE('20160801000000','YYYYMMDDHH24MISS') --this should be replaced with startdate parameter
AND day_date < TO_DATE('20160831000000','YYYYMMDDHH24MISS') --this should be replaced with enddate parameter
)
-
-- number of holidays (after a given date)
(
SELECT COUNT(1)
FROM HOLIDAYS
WHERE HOLIDAY_DATE > TO_DATE('20160801000000','YYYYMMDDHH24MISS') --this should be replaced with startdate parameter
AND HOLIDAY_DATE < TO_DATE('20160831000000','YYYYMMDDHH24MISS') --this should be replaced with enddate parameter
)
INTO WORKINGDAYS_BETWEEN
FROM DUAL;
RETURN WORKINGDAYS_BETWEEN;
END NET_WORKING_DAYS;
**EDIT-1: Holidays are already defined in HOLIDAYS table in the database and for this date range from 20160801000000 to 20160831000000 , 30.06.2016 is the holiday date.
You do not need to use a row generator to enumerate every day to get the number of week days - it can be done using a simple calculation:
From my answer here:
CREATE FUNCTION getWorkingDays (
in_start_date IN DATE,
in_end_date IN DATE
) RETURN NUMBER DETERMINISTIC
IS
p_start_date DATE;
p_end_date DATE;
p_working_days NUMBER;
p_holiday_days NUMBER;
BEGIN
IF in_start_date IS NULL OR in_end_date IS NULL THEN
RETURN NUll;
END IF;
p_start_date := LEAST( in_start_date, in_end_date );
p_end_date := GREATEST( in_start_date, in_end_date );
-- 5/7 * ( Number of days between monday of the week containing the start date
-- and monday of the week containing the end date )
-- + LEAST( day of week for end date, 5 )
-- - LEAST( day of week for start date, 5 )
p_working_days := ( TRUNC( p_end_date, 'IW' ) - TRUNC( p_start_date, 'IW' ) ) * 5 / 7
+ LEAST( p_end_date - TRUNC( p_end_date, 'IW' ), 5 )
- LEAST( p_start_date - TRUNC( p_start_date, 'IW' ), 5 );
SELECT COALESCE(
SUM(
LEAST( p_end_date, holiday_date + INTERVAL '1' DAY )
- GREATEST( p_start_date, holiday_date )
),
0
)
INTO p_holiday_days
FROM Holidays
WHERE HOLIDAY_DATE BETWEEN TRUNC( p_start_date )
AND TRUNC( p_end_date )
AND HOLIDAY_DATE - TRUNC( HOLIDAY_DATE, 'IW' ) < 5;
RETURN GREATEST( p_working_days - p_holiday_days, 0 );
END;
/
I have all week numbers from 20161 to 201640. I want to know what is the start date and end date of week 31.
How can I write query in postgresql to get that?
To get the start date, simply convert the week number to a date using to_date()
If you are using ISO week numbers use:
select to_date('201643', 'iyyyiw');
Otherwise use:
select to_date('201643', 'yyyyww');
To get the end date, just add 7 to resulting date: to_date('201643', 'iyyyiw') + 7
SELECT date '2016-01-01' + interval '1 day' * ((7 - EXTRACT(DOW FROM DATE '2016-01-01')) + 29*7) AS start_date,
date '2016-01-01' + interval '1 day' * ((7 - EXTRACT(DOW FROM DATE '2016-01-01')) + 29*7 + 6) AS end_date,
Dealing with weeks may be tricky if you allow the first and last weeks of the year to be less than 7 days long.
This is my two cents using ISO weeks (first day is monday, with dow = 1). They return the first and last date of a week out of the year and the week index.
Notice a year has 365/7 = 52,142857142857143 weeks, or 366/7 = 52,285714285714286 weeks, depending on its length. So, weeks always range in [0, 52]. I had to use an if for week 52 as it cannot be calculated as 7-d.
create or replace function first_date_of_isoweek(y int, w int)
returns date
language sql as $$
select to_date(concat(y, to_char(w, 'fm00')), 'iyyyiw');
$$;
create or replace function last_date_of_isoweek(y int, w int)
returns date
language plpgsql as $$
declare
d1 date;
d2 date;
d smallint;
begin
-- Calculate first date of an isoweek
d1 = to_date(concat(y, to_char(w, 'fm00')), 'iyyyiw');
-- Year's last week needs an speacial treatment
if w = 52 then
return to_date(concat(y, '1231'), 'yyyyMMdd');
else
-- Calculate the dow of the first date
d = extract(isodow from d1);
-- Calculate the last date out of the first date
return d1 + interval '1 day' * (7-d);
end if;
end $$;
Test 2021 as follows:
select first_date_of_isoweek(2021, 0);
first_date_of_isoweek|
---------------------+
2021-01-01|
select last_date_of_isoweek(2021, 0);
last_date_of_isoweek|
--------------------+
2021-01-03|
select first_date_of_isoweek(2021, 1);
first_date_of_isoweek|
---------------------+
2021-01-04|
select last_date_of_isoweek(2021, 1);
last_date_of_isoweek|
--------------------+
2021-01-10|
select first_date_of_isoweek(2021, 52);
first_date_of_isoweek|
---------------------+
2021-12-27|
select last_date_of_isoweek(2021, 52);
last_date_of_isoweek|
--------------------+
2021-12-31|
I have a function:
CREATE OR REPLACE FUNCTION a(b integer)
RETURNS integer AS
$BODY$
declare
c integer;
timevalue text;
begin
timevalue = b::text || ' days'; -- build string like '7 days'
select sum(value)
into c
from tablx
where createdate between current_date - interval timevalue and current_date;
return c;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
function is simple... give summary of records which fits the date criteria.
for some reason it does not accept current_date - interval timevalue
what can I do?
Unfortunately you can't specify a "dynamic" interval value. But as you always use the same unit, you can use:
select sum(value)
into c
from tablx
where createdate between current_date - interval '1' day * b and current_date;
You can make that simpler, because you can subtract b directly from current_date
select sum(value)
into c
from tablx
where createdate between current_date - b and current_date;
In an expression date - integer the integer value is the number of days.
For more details see the manual: http://www.postgresql.org/docs/current/static/functions-datetime.html