I am reviewing the functions but I cannot solve this one;
Write a function that returns every Friday 13th during a specific year.
Example:
SELECT * FROM martes13(2020);
13/01/2020
13/03/2020
13/08/2020
My unfortunate attempt, do not pay much attention.
DECLARE
diaInicial date;
diaFinal date;
anio1 date;
anio2 date;
auxData date;
dates date[];
BEGIN
diaInicial := ('01/' || '01/' || anio ) :: date;
diaFinal := diaInicial + '1 YEAR' :: interval;
anio2:= date_part('year',diaFinal);
FOR i IN 1..12 BY 1 LOOP
FOR j IN 1..30 BY 1 LOOP
diaInicial := anio || '-' || i || '-' || j;
if(date_part('dom',auxData)==13 and date_part('dow',auxData)==5)then
dates[j] := diaInicial;
end if;
end loop;
end loop;
return dates;
END;
There's no way to solve it no matter how hard I try, I understand that I have to use dates, years intervals and counters but it does not work out. Any help or information could be of use to me.
Thanks in advance.
That can be solved with a simple SQL statement:
SELECT CAST(d AS date)
FROM generate_series(
TIMESTAMP '2020-01-13',
TIMESTAMP '2020-12-13',
INTERVAL '1 month'
) AS thirteen(d)
WHERE EXTRACT (dow FROM d) = 5;
You could wrap that in an SQL function.
Related
I need to find the 'n'th business day in oracle plsql function which should exclude weekends and custom list of holidays. I got something like this however unable to fit the custom holiday logic in here,
create or replace function add_n_working_days (
start_date date, working_days pls_integer
) return date as
end_date date := start_date;
counter pls_integer := 0;
begin
if working_days = 0 then
end_date := start_date;
elsif to_char(start_date, 'fmdy') in ('sat', 'sun') then
end_date := next_day(start_date, 'monday');
end if;
while (counter < working_days) loop
end_date := end_date + 1;
if to_char(end_date, 'fmdy') not in ('sat', 'sun') then
counter := counter + 1;
end if;
end loop;
return end_date;
end add_n_working_days;
/
I have the custom holiday list in a different table which can be extracted using the sql
select holidays from holiday_table
I tried adding elsif condition with a sub query but that is not supported
if to_char(end_date, 'fmdy') not in ('sat', 'sun') then
counter := counter + 1;
elsif to_char(end_date, 'YYYYMMDD') in (select holidays from holiday_table) then
counter := counter + 1;
end if;
I will try with counting the number of weekend days with a similar loop:
WHILE (v_copy > 0) LOOP
end_date := end_date + 1;
IF to_char(end_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := end_date + 1;
ELSE
v_copy := v_copy - 1;
END IF;
END LOOP;
And then after the looping process you can count the number of holidays in the table which are after the start_date and end_date and not in ('sat', 'sun').
SELECT COUNT(*) INTO v_custom_cnt
FROM holiday_table
WHERE to_char(holidays, 'fmdy') not in ('sat', 'sun') AND
holidays BETWEEN start_date AND end_date;
Now add this number to your end_date and you will get the date of the nth business day. Of course before returning check if the new end_date is in ('sat', 'sun').
end_date := end_date + v_custom_cnt;
IF to_char(holidays, 'fmdy') in ('sat', 'sun') THEN
end_date := next_day(end_date, 'monday');
END IF;
Finally the end_date is the date of the nth working day.
CREATE OR REPLACE FUNCTION add_n_working_days (
start_date DATE,
working_days PLS_INTEGER
) RETURN DATE AS
end_date DATE := start_date;
counter PLS_INTEGER := 0;
v_copy PLS_INTEGER := working_days;
v_custom_cnt INTEGER := 0;
BEGIN
IF working_days = 0 THEN
end_date := start_date;
ELSIF to_char(start_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := next_day(start_date, 'monday');
END IF;
WHILE (v_copy > 0) LOOP
end_date := end_date + 1;
IF to_char(end_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := end_date + 1;
ELSE
v_copy := v_copy - 1;
END IF;
END LOOP;
SELECT COUNT(*) INTO v_custom_cnt
FROM holiday_table
WHERE to_char(end_date, 'fmdy') NOT IN ('sat', 'sun')
AND holidays BETWEEN start_date AND end_date;
end_date := end_date + v_custom_cnt;
IF to_char(end_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := next_day(start_date, 'monday');
END IF;
RETURN end_date;
END add_n_working_days;
It is not 100% tested
Different approach: create a table that only lists business day and take nth value:
CREATE OR REPLACE FUNCTION add_n_working_days (
start_date DATE, working_days PLS_INTEGER
) RETURN DATE AS
l_end_date DATE := start_date;
l_counter pls_integer := 0;
BEGIN
SELECT
business_day
INTO l_end_date
FROM
(
WITH
dates AS
(SELECT start_date + level - 1 as dt FROM dual CONNECT BY level < 100)
,weekdates AS
(SELECT dt as weekday FROM dates WHERE TO_CHAR(dt,'fmdy') NOT IN ('sat','sun'))
,business_days AS
(
SELECT weekday as business_day FROM weekdates
MINUS
SELECT holiday FROM so_holidays
)
SELECT business_day, ROW_NUMBER() OVER (ORDER BY 1) as rn from business_days
)
WHERE rn = working_days + 1;
RETURN l_end_date;
END add_n_working_days;
SQL is set orientated so start thinking in terms of sets NOT iteration (no loops). Assuming you can designate a maximum number of days to be tested you can get what you want with a single simple query.
create or replace function add_n_working_days (
start_date date, working_days pls_integer
) return date as
working_date_ot date;
begin
with date_list as (select trunc(start_date) + level - 1 tdate
from dual
connect by level <= 5*working_days
)
select tdate
into working_date_ot
from ( select tdate, row_number() over(order by tdate) date_num
from date_list
where to_char(tdate,'fmdy') not in ('sat','sun')
and not exists
(select null
from holiday_dates
where trunc(holiday_date) = tdate
)
)
where date_num = working_days;
return working_date_ot;
end add_n_working_days;
How it works:
The function assumes the desired date exists within 5 times the number of days requested. So if 10 business hence are requested the resulting date will be within 50 days (that would be a whole lot of holidays if not).
The date_list CTE builds a list of potential dates out to the identified limit.
The sub select in the main filters the generated list eliminating Sat and Sun. It then probes the holiday table for the remaining dates and eliminating any any date in the table.
Remaining dates are numbered based on ascending value.
The main outer select then chooses the date number matching the working_days specified.
No Looping required. Just my .02 cents worth.
I would like to declare some variables that contain the first and last date of the current month in Oracle. I know how to get these values, but evidently not store or use them; I am more of a T-SQL guy.
In T-SQL, I could write:
DECLARE #startDate DATE = GETDATE();
DECLARE #endDate DATE = EOMONTH(GETDATE());
SELECT *
FROM SomeTable
WHERE SomeDate BETWEEN #startDate AND #endDate;
I cannot, for the life of me, work out how to do this in Oracle. I have tried several variations, like:
DECLARE END_DT DATE := TRUNC(LAST_DAY(SYSDATE))
END_DT DATE := TRUNC(LAST_DAY(SYSDATE))
DECLARE END_DT DATE;
SELECT TRUNC(LAST_DAY(SYSDATE)) INTO END_DT FROM DUAL;
I am mostly using 11g, but I would like to be able to use the same script on a 9i/10g server also.
You can use such a query to detect first and last days of the current month :
SELECT TRUNC(LAST_DAY(ADD_MONTHS(SYSDATE,-1)))+1,
TRUNC(LAST_DAY(SYSDATE))
INTO :startDate,:endDate
FROM DUAL;
with the contribution of ADD_MONTHS() function
Update : Alternatively use this PL/SQL code block :
DECLARE
startDate date;
endDate date;
BEGIN
startDate := TRUNC(LAST_DAY(ADD_MONTHS(SYSDATE,-1)))+1;
endDate := TRUNC(LAST_DAY(SYSDATE));
DBMS_OUTPUT.PUT_LINE ('startDate : '||startDate);
DBMS_OUTPUT.PUT_LINE ('endDate : '||endDate);
END;
/
Demo
You can use the LAST_DAY and TRUNC combination as following:
DECLARE
START_DT DATE := TRUNC(SYSDATE, 'MM');
END_DT DATE := TRUNC(LAST_DAY(SYSDATE));
BEGIN
--DBMS_OUTPUT.PUT_LINE(START_DT || ' ' || END_DT);
SELECT
*
INTO <...>
FROM
SOMETABLE
WHERE
SOMEDATE BETWEEN START_DT AND END_DT;
END;
/
Cheers!!
I have Holiday table that contains all holidays in it.
How can I get next work day from given date in one SQL query and not use FOR Loop like below?
DECLARE
givendate DATE := TO_DATE('2019-07-01', 'YYYY-MM-DD');
out NUMBER := 1;
BEGIN
WHILE out != 0 LOOP
SELECT
COUNT(*)
INTO out
FROM
holiday h
WHERE
trunc(h.holiday_date) = trunc(givendate);
IF out != 0 THEN
givendate := givendate + 1;
END IF;
END LOOP;
DBMS_OUTPUT.put_line(givendate);
END;
SELECT MIN(hd)
FROM (
select TO_DATE('2019-07-01', 'YYYY-MM-DD') as hd from dual
union
select trunc(h.holiday_date + 1)
FROM
holiday h
WHERE
trunc(h.holiday_date) >= TO_DATE('2019-07-01', 'YYYY-MM-DD')
) t left join holiday h2 on trunc(h2.holiday_date) = t.hd
WHERE h2.holiday_date IS NULL;
You may want to use a hierarchical query:
select nvl(max(holiday_date)+1, trunc(sysdate))
from holiday
connect by holiday_date = prior holiday_date + 1
start with holiday_date = trunc(sysdate)
It works like that:
if sysdate is a holiday, it builds a chain of continuous holidays and then returns max holiday_date + 1 as thenext business date
if sysdate is not a holiday, it returns sysdate (nvl() does that when max returns null)
So just replace trunc(sysdate) with your given_date and it should work just as your piece of code.
I'm trying to make a function on psql. It will be triggered on insert on table. I want to inject an variable on my select. Can't get working ...
CREATE OR REPLACE FUNCTION updateHistoricLongTime()
RETURNS void AS $$
DECLARE
hour_nb int;
index_hour int;
saved_hours int;
tmp_counter int;
BEGIN
hour_nb := 0;
index_hour := 1;
saved_hours := 2160;
tmp_counter := 0;
SELECT COUNT(*) FROM locationhistoric WHERE type='hour' AND idLocation=6 INTO hour_nb;
IF (hour_nb<saved_hours) THEN
FOR i IN 1 .. saved_hours LOOP
SELECT COUNT(*) FROM visits
WHERE stend < (timestamp '2017-11-29 15:00' - interval **>> index_hour<<<** - 1 ' hour') AND stend > (timestamp '017-11-29 15:00' - interval **>>index_hour <<<**' hour') AND location_id=6 AND duration>0 INTO tmp_counter;
index_hour := index_hour + 1;
END LOOP;
END IF;
END;
$$
LANGUAGE 'plpgsql' IMMUTABLE;
How can I inject variable index_hour in my SELECT COUNT(*) FROM Visits ...
EDIT: It's just syntax issue, but I can't manage to find the right way !
The result in command line:
ERROR: syntax error at or near "index_hour"
LINE 16: ... stend < (timestamp '2017-11-29 15:00' - interval index_hour...
Thanks a lot,
The solution
CREATE OR REPLACE FUNCTION updateHistoricLongTime()
RETURNS void AS $$
DECLARE
hour_nb int;
index_hour int;
saved_hours int;
tmp_counter int;
index_hour_minor int;
BEGIN
hour_nb := 0;
index_hour := 1;
index_hour_minor := 0;
saved_hours := 2160;
SELECT COUNT(*)
INTO hour_nb
FROM locationhistoric
WHERE type='hour'
AND idLocation=6;
IF (hour_nb<saved_hours) THEN
FOR i IN 1 .. saved_hours LOOP
SELECT COUNT(*)
INTO tmp_counter
FROM visits
WHERE start > timestamp '2017-11-29 15:00' - ( interval '1 hour' * index_hour )
AND start < timestamp '2017-11-29 15:00' - ( interval '1 hour' * index_hour_minor)
AND location_id=6
AND duration>0;
INSERT INTO locationhistoric
(type, date, counter, idLocation)
VALUES( 'hour',
timestamp '2017-11-29 15:00' - ( interval '1 hour' * index_hour_minor),
tmp_counter,
6);
index_hour_minor := index_hour_minor + 1;
index_hour := index_hour + 1;
END LOOP;
END IF;
END;
$$
LANGUAGE plpgsql;
The value specified for an interval can't be passed as a variable. However if the base unit is always an hour you can multiply a one our interval with the desired number of ours, e.g.
interval '1' hour * 5
will return 5 hours. The 5 can be a parameter. So your query should be:
SELECT COUNT(*)
INTO tmp_counter
FROM visits
WHERE stend < (timestamp '2017-11-29 15:00' - (interval '1' hour * index_hour))
AND stend > (timestamp '2017-11-29 15:00' - (interval '1' hour * index_hour))
AND location_id=6
AND duration > 0;
The syntax you want to get for your query (where index_hour = 8, for example) is:
select count(*)
from visits
where
stend < (timestamp '2017-11-29 15:00' - interval '7 hour') and
stend > (timestamp '2017-11-29 15:00' - interval '8 hour') and
location_id = 6 and
duration > 0;
Note where the quotes are. This means that your variable has to be inside quotes in pl/pgsql and that means it will be treated as a literal.
The solution is:
execute
'select count(*) ' ||
'from visits ' ||
'where ' ||
'stend < (timestamp ''2017-11-29 15:00'' - interval ''' || (index_hour - 1) || ' hour'') and ' ||
'stend > (timestamp ''2017-11-29 15:00'' - interval ''' || index_hour || ' hour'') and ' ||
'location_id = 6 and ' ||
'duration > 0'
To save me setting up your data I've written a simpler example using a table that I have (driver) so that I could test. Note that you have to use 2 single quotes to get one single quote into a string and that means counting quotes carefully.
create function a47768241() returns integer
as $body$
declare
index_hour int;
id integer;
begin
index_hour = 8;
execute
'select id ' ||
'from driver ' ||
'where ' ||
'from_date_time < (timestamp ''2013-04-22 16:00:00'' - interval ''' || (index_hour - 1) || ' hour'') '
into id;
return id;
end;
$body$
language 'plpgsql';
Simple test:
# select a47768241();
a47768241
-----------
158
(1 row)
Using the result value to check the date:
# select * from driver where id = a47768241();
id | vehicle_id | person_id | from_date_time | to_date_time | created_at | updated_at
-----+------------+-----------+---------------------------+---------------------------+----------------------------+------------
158 | 6784 | 15430 | 2012-09-13 17:00:41.39778 | 2012-09-14 01:54:46.39778 | 2016-06-03 16:43:11.456063 |
(1 row)
just concat the interval value, like
interval concat(index_hour - 1 , ' hour')
Below is my function where i tried to calculate the number of working days between two dates.
CREATE OR REPLACE function workingdays
( start_date IN DATE , end_date IN DATE)
return number
as
total_days varchar2(10);
v_count integer:= 0;
end_value date := to_date(end_date , 'YYYY-MM-DD');
start_value date := to_date(start_date , 'YYYY-MM-DD');
date_diff number(10);
begin
while(start_value <= end_value)
loop
if (to_char(TO_DATE(start_value,'YYYY-MM-DD') , 'D') = 1 or to_char(TO_DATE(start_value,'YYYY-MM-DD'), 'D') = 7)
then v_count := v_count+1;
end if;
start_value := start_value + 1;
end loop;
date_diff :=to_number( to_date( end_date, 'YYYY-MM-DD') - to_date (start_date , 'YYYY-MM-DD')) ;
total_days := to_char( (to_number(date_diff) - to_number(v_count)) + 1);
return (' The total working days is' || to_number((total_days)));
end;
/
The function compiles successfully, but on executing it, I get an error message at the return line. Could someone guide me. It is some issue the number/character conversion.
You make a lot of useless conversions, almost all of them can be skipped.
Result of TO_CHAR(..., 'D') depends on current user NLS_TERRITORY value, so you should not use it unless you ensure a certain NLS_TERRITORY setting.
This one is working:
CREATE OR REPLACE FUNCTION workingdays(start_date IN DATE , end_date IN DATE) RETURN VARCHAR2 AS
total_days NUMBER;
v_count INTEGER:= 0;
end_value DATE := TRUNC(end_date);
start_value DATE := TRUNC(start_date);
date_diff NUMBER;
BEGIN
WHILE start_value <= end_value LOOP
IF TO_CHAR(start_value, 'fmDay', 'NLS_DATE_LANGUAGE = american') IN ('Saturday','Sunday') THEN
v_count := v_count + 1;
END IF;
start_value := start_value + 1;
END LOOP;
date_diff := end_value - TRUNC(start_date);
total_days := date_diff - v_count + 1;
RETURN ' The total working days is ' || total_days;
END;
/
There are many other possibilities, this is just another way of doing it:
CREATE OR REPLACE FUNCTION workingdays(start_date IN DATE, end_date IN DATE) RETURN VARCHAR2 AS
next_run_date DATE := TRUNC(start_date);
total_days INTEGER := 0;
BEGIN
LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=DAILY;INTERVAL=1;BYDAY=MON,TUE,WED,THU,FRI', NULL, next_run_date, next_run_date);
EXIT WHEN next_run_date >= end_date;
total_days := total_days + 1;
END LOOP;
RETURN ' The total working days is ' || total_days;
END;
select min(a_day)
as from_date
,max(a_day)
as till_date
,sum(decode(to_char(a_day,'D')
,1,0
,7,0
,1
)
)
as wrk_days_count
from (select trunc(sysdate,'iw') + level - 1
as a_day
from dual
connect by level < 8)
;