Oracle get last weekday Mon-Fri - sql

I would like to obtain the last weekday.
If it's Tues to Sat, it will be the previous day. If it's Sunday or Monday, it will be Friday.
So far, I've tried this, but I'm struggling to get the desired output.
SELECT
level AS dow,
trunc(sysdate, 'D') + level day,
to_char(trunc(sysdate, 'D') + level, 'Day') AS day_week,
CASE
WHEN to_char(trunc(sysdate, 'D') + level, 'Day') IN (
'Sunday',
'Monday'
) THEN
trunc(sysdate - 2, 'IW') + 4
ELSE
sysdate - 1
END calculation
FROM
dual
CONNECT BY
level <= 7;

This solution works independent of language and territory:
SELECT date_value,
date_value - CASE TRUNC(date_value) - TRUNC(date_value, 'IW')
WHEN 0 THEN 3 -- Monday
WHEN 6 THEN 2 -- Sunday
ELSE 1 -- Tuesday to Saturday
END AS previous_weekday
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name (date_value) AS
SELECT TRUNC(sysdate - LEVEL + 1)
FROM DUAL
CONNECT BY LEVEL <= 7;
Outputs (with the date format YYYY-MM-DD HH24:MI:SS (DY)):
DATE_VALUE
PREVIOUS_WEEKDAY
2021-07-20 00:00:00 (TUE)
2021-07-19 00:00:00 (MON)
2021-07-19 00:00:00 (MON)
2021-07-16 00:00:00 (FRI)
2021-07-18 00:00:00 (SUN)
2021-07-16 00:00:00 (FRI)
2021-07-17 00:00:00 (SAT)
2021-07-16 00:00:00 (FRI)
2021-07-16 00:00:00 (FRI)
2021-07-15 00:00:00 (THU)
2021-07-15 00:00:00 (THU)
2021-07-14 00:00:00 (WED)
2021-07-14 00:00:00 (WED)
2021-07-13 00:00:00 (TUE)
db<>fiddle here

Related

Extract 5th Business Date from sysdate

I'm looking to extract the 5th Business Date data from database. Looking for pure 5th Business Date, no other business requirement like Holiday or New Year day.
Looking to extract 07/03/2022 from dual table using Oracle PL/SQL
Date
Day
Requirement
1/03/2022
Tuesday
1BD
2/03/2022
Wednesday
2BD
3/03/2022
Thursday
3BD
4/03/2022
Friday
4BD
5/03/2022
Saturday
Weekend
6/03/2022
Sunday
Weekend
7/03/2022
Monday
5BD
8/03/2022
Tuesday
6BD
9/03/2022
Wednesday
7BD
This is how I understood it.
Today is Thursday, 24.03.2022. It means that 5th business day looking backwards is Friday, 18.03.2022.
SQL> with test (datum, day) as
2 -- calendar
3 (select
4 trunc(sysdate) - &&par_number_of_days * 2 + level - 1,
5 to_char(trunc(sysdate) - &&par_number_of_days * 2 + level - 1, 'dy',
6 'nls_date_language = english')
7 from dual
8 connect by level <= (&&par_number_of_days * 2) + 1
9 ),
10 only_working_days as
11 -- remove weekends
12 (select datum,
13 day,
14 row_number() over (order by datum desc) rn
15 from test
16 where day not in ('sat', 'sun')
17 )
18 select datum, day, rn
19 from only_working_days
20 where rn = &&par_number_of_days;
Enter value for par_number_of_days: 5
DATUM DAY RN
---------- --- ----------
18.03.2022 fri 5
Or, 13th business day backwards is 08.03.2022:
SQL> undefine par_number_of_days
SQL> /
Enter value for par_number_of_days: 13
DATUM DAY RN
---------- --- ----------
08.03.2022 tue 13
SQL>
If it is, on the other hand, related to period since 1st of current, month, then
SQL> with test (datum, day) as
2 (select trunc(sysdate, 'mm') + level - 1,
3 to_char(trunc(sysdate, 'mm') + level - 1, 'dy', 'nls_date_language = english')
4 from dual
5 connect by level <= trunc(sysdate) - trunc(sysdate, 'mm') + 1
6 ),
7 only_working_days as
8 -- remove weekends
9 (select datum,
10 day,
11 row_number() over (order by datum) rn
12 from test
13 where day not in ('sat', 'sun')
14 )
15 select datum, day, rn
16 from only_working_days
17 where rn = &par_number_of_days;
Enter value for par_number_of_days: 5
DATUM DAY RN
---------- --- ----------
07.03.2022 mon 5
SQL> /
Enter value for par_number_of_days: 13
DATUM DAY RN
---------- --- ----------
17.03.2022 thu 13
SQL>
The 5th business day will always be 7 days ahead, since there will be 5 weekdays and 2 weekend days, so the simplest solution is:
SELECT TRUNC(SYSDATE) + INTERVAL '7' DAYS
FROM DUAL
More generally, if you want to add a number of business days to a date then you can calculate it using:
start_date
+ FLOOR(bd/5) * INTERVAL '7' DAY -- Full weeks
+ MOD(bd, 5) -- Part week
+ CASE
WHEN start_date - TRUNC(start_date, 'IW') + MOD(bd, 5) >= 5
THEN 2
WHEN start_date - TRUNC(start_date, 'IW') + MOD(bd, 5) < 0
THEN -2
ELSE 0
END -- Adjust for weekend
For example, given the sample data:
CREATE TABLE table_name (start_date, bd) AS
SELECT TRUNC(SYSDATE), LEVEL - 11 FROM DUAL CONNECT BY LEVEL <= 21
UNION ALL
SELECT DATE '2022-03-01', 5 FROM DUAL;
Then:
SELECT start_date,
bd,
start_date
+ FLOOR(bd/5) * INTERVAL '7' DAY -- Full weeks
+ MOD(bd, 5) -- Part week
+ CASE
WHEN start_date - TRUNC(start_date, 'IW') + MOD(bd, 5) >= 5
THEN 2
WHEN start_date - TRUNC(start_date, 'IW') + MOD(bd, 5) < 0
THEN -2
ELSE 0
END -- Adjust for weekend
AS adjusted_business_day
FROM table_name;
Outputs:
START_DATE
BD
ADJUSTED_BUSINESS_DAY
2022-03-24 00:00:00 (THU)
-10
2022-03-10 00:00:00 (THU)
2022-03-24 00:00:00 (THU)
-9
2022-03-04 00:00:00 (FRI)
2022-03-24 00:00:00 (THU)
-8
2022-03-07 00:00:00 (MON)
2022-03-24 00:00:00 (THU)
-7
2022-03-08 00:00:00 (TUE)
2022-03-24 00:00:00 (THU)
-6
2022-03-09 00:00:00 (WED)
2022-03-24 00:00:00 (THU)
-5
2022-03-17 00:00:00 (THU)
2022-03-24 00:00:00 (THU)
-4
2022-03-11 00:00:00 (FRI)
2022-03-24 00:00:00 (THU)
-3
2022-03-14 00:00:00 (MON)
2022-03-24 00:00:00 (THU)
-2
2022-03-15 00:00:00 (TUE)
2022-03-24 00:00:00 (THU)
-1
2022-03-16 00:00:00 (WED)
2022-03-24 00:00:00 (THU)
0
2022-03-24 00:00:00 (THU)
2022-03-24 00:00:00 (THU)
1
2022-03-25 00:00:00 (FRI)
2022-03-24 00:00:00 (THU)
2
2022-03-28 00:00:00 (MON)
2022-03-24 00:00:00 (THU)
3
2022-03-29 00:00:00 (TUE)
2022-03-24 00:00:00 (THU)
4
2022-03-30 00:00:00 (WED)
2022-03-24 00:00:00 (THU)
5
2022-03-31 00:00:00 (THU)
2022-03-24 00:00:00 (THU)
6
2022-04-01 00:00:00 (FRI)
2022-03-24 00:00:00 (THU)
7
2022-04-04 00:00:00 (MON)
2022-03-24 00:00:00 (THU)
8
2022-04-05 00:00:00 (TUE)
2022-03-24 00:00:00 (THU)
9
2022-04-06 00:00:00 (WED)
2022-03-24 00:00:00 (THU)
10
2022-04-07 00:00:00 (THU)
2022-03-01 00:00:00 (TUE)
5
2022-03-08 00:00:00 (TUE)
db<>fiddle here

Get quarter start/end dates for more than a year (start year to current year)

I've been trying to get start and end dates range for each quarter given a specific date/year, like this:
SELECT DATEADD(mm, (quarter - 1) * 3, year_date) StartDate,
DATEADD(dd, 0, DATEADD(mm, quarter * 3, year_date)) EndDate
--quarter QuarterNo
FROM
(
SELECT '2012-01-01' year_date
) s CROSS JOIN
(
SELECT 1 quarter UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4
) q
which produces the following output:
2012-01-01 00:00:00 2012-04-01 00:00:00
2012-04-01 00:00:00 2012-07-01 00:00:00
2012-07-01 00:00:00 2012-10-01 00:00:00
2012-10-01 00:00:00 2013-01-01 00:00:00
Problem: I need to do this for a given start_date and end_date, the problem being the end_date=current_day, so how can I achieve this:
2012-01-01 00:00:00 2012-04-01 00:00:00
2012-04-01 00:00:00 2012-07-01 00:00:00
2012-07-01 00:00:00 2012-10-01 00:00:00
2012-10-01 00:00:00 2013-01-01 00:00:00
... ...
2021-01-01 00:00:00 2021-01-06 00:00:00
I think here is what you want to do :
SET startdatevar AS DATEtime = '2020-01-10'
;WITH RECURSIVE cte AS (
SELECT startdatevar AS startdate , DATEADD(QUARTER, 1 , startdatevar) enddate , 1 quarter
UNION ALL
SELECT enddate , CASE WHEN DATEADD(QUARTER, 1 , enddate) > CURRENT_DATE() THEN GETDATE() ELSE DATEADD(QUARTER, 1 , enddate) END enddate, quarter + 1
FROM cte
WHERE
cte.enddate <= CURRENT_DATE()
and quarter < 4
)
SELECT * FROM cte
to use your code , if you want to have more than 4 quarters :
SET quarter_limit = DATEDIFF(quarter , <startdate>,<enddate>)
;WITH RECURSIVE cte(q, qDate,enddate) as
(
select 1,
DATEFROMPARTS(year('2012-01-01'::date), 1, 1) -- First quarter date
,time_slice('2012-01-01'::date, 3, 'MONTH', 'END')
UNION ALL
select q+1,
DATEADD(q, 1, qdate) -- next quarter start date
,time_slice(qdate::date, (q+1)*3, 'MONTH', 'END')
from cte
where q < quarter_limit -- limiting the number of next quarters
AND cte.endDate <= <enddate>
)
SELECT * FROM cte
After #eshirvana's answer, I came up with this slightly change after your answer:
WITH RECURSIVE cte(q, qDate,enddate) as
(
select 1,
DATEFROMPARTS(year('2012-01-01'::date), 1, 1) -- First quarter date
,time_slice('2012-01-01'::date, 3, 'MONTH', 'END')
UNION ALL
select q+1,
DATEADD(q, 1, qdate) -- next quarter start date
,time_slice(qdate::date, (q+1)*3, 'MONTH', 'END')
from cte
where q <4 -- limiting the number of next quarters
AND cte.endDate <= CURRENT_DATE()
)
SELECT * FROM cte
Which works fine for whatever year I pass there (2012 will produce 4 records, 2021 just one, since we're still on the first quarter right now).
[EDIT]: it still doesn't work as expected after your 2nd code sugestion:
WITH RECURSIVE cte(q, qDate,enddate) as
(
select 1,
DATEFROMPARTS(year('2012-01-01'::date), 1, 1) -- First quarter date
,CASE WHEN time_slice('2012-01-01'::date, 3, 'MONTH', 'END') > CURRENT_DATE
THEN current_date
ELSE time_slice('2012-01-01'::date, 3, 'MONTH', 'END')
END
UNION ALL
select q+1,
DATEADD(q, 1, qdate) -- next quarter start date
,time_slice(qdate::date, (q+1)*3, 'MONTH', 'END')
from cte
where q < DATEDIFF(quarter , '2012-01-01'::date,'2021-01-06'::date)
AND cte.endDate <= '2021-01-06'::date
)
SELECT * FROM cte
is outputing this:
Sorry #eshirvana, it doesn't work as expected though. It all goes well to some point, but it's not returning all the records. Instead, it produces less records and wrong one, like this:
1 2012-01-01 2012-04-01
2 2012-04-01 2012-07-01
3 2012-07-01 2012-10-01
4 2012-10-01 2013-01-01
5 2013-01-01 2013-10-01
6 2013-04-01 2013-07-01
7 2013-07-01 2013-10-01
8 2013-10-01 2014-01-01
9 2014-01-01 2015-01-01
10 2014-04-01 2015-01-01
11 2014-07-01 2016-10-01
12 2014-10-01 2015-01-01
13 2015-01-01 2015-07-01
14 2015-04-01 2015-07-01
15 2015-07-01 2018-10-01
16 2015-10-01 2018-01-01
17 2016-01-01 2016-10-01
18 2016-04-01 2019-07-01
19 2016-07-01 2017-07-01
20 2016-10-01 2020-01-01
21 2017-01-01 2017-04-01
22 2017-04-01 2019-07-01
23 2017-07-01 2021-10-01
Although my logic it's still not ok for not printing just Q1 dates for 2021, could this output issues be related to date format or something?
Now, it seems to be working, at least for 2012-01-01 till today (2021-01-06).
The code :
WITH RECURSIVE cte(q, qDate,enddate) as
(
select
-- it might not be the first quarter, so better to protect that:
quarter('2012-01-01'::date)::numeric
, DATEFROMPARTS(year('2012-01-01'::date), 1, 1) -- First quarter date
, CASE WHEN time_slice('2012-01-01'::date, 3, 'MONTH', 'END') > '2021-01-06'::date
THEN '2021-01-06'::date
ELSE time_slice('2012-01-01'::date, 3, 'MONTH', 'END')
END
UNION ALL
select q+1
, DATEADD(q, 1, qdate) -- next quarter start date
,CASE WHEN time_slice(DATEADD(q, 1, qdate), 3, 'MONTH', 'END')> '2021-01-06'::date
THEN '2021-01-06'::date
ELSE time_slice(DATEADD(q, 1, qdate), 3, 'MONTH', 'END')
END
from cte
where q <= DATEDIFF(quarter , '2012-01-01'::date,'2021-01-06'::date)
AND cte.endDate <= '2021-01-06'::date
)
SELECT * FROM cte
The output:
1 2012-01-01 2012-04-01
2 2012-04-01 2012-07-01
3 2012-07-01 2012-10-01
4 2012-10-01 2013-01-01
5 2013-01-01 2013-04-01
6 2013-04-01 2013-07-01
7 2013-07-01 2013-10-01
8 2013-10-01 2014-01-01
9 2014-01-01 2014-04-01
10 2014-04-01 2014-07-01
11 2014-07-01 2014-10-01
12 2014-10-01 2015-01-01
13 2015-01-01 2015-04-01
14 2015-04-01 2015-07-01
15 2015-07-01 2015-10-01
16 2015-10-01 2016-01-01
17 2016-01-01 2016-04-01
18 2016-04-01 2016-07-01
19 2016-07-01 2016-10-01
20 2016-10-01 2017-01-01
21 2017-01-01 2017-04-01
22 2017-04-01 2017-07-01
23 2017-07-01 2017-10-01
24 2017-10-01 2018-01-01
25 2018-01-01 2018-04-01
26 2018-04-01 2018-07-01
27 2018-07-01 2018-10-01
28 2018-10-01 2019-01-01
29 2019-01-01 2019-04-01
30 2019-04-01 2019-07-01
31 2019-07-01 2019-10-01
32 2019-10-01 2020-01-01
33 2020-01-01 2020-04-01
34 2020-04-01 2020-07-01
35 2020-07-01 2020-10-01
36 2020-10-01 2021-01-01
37 2021-01-01 2021-01-06
In case you're wondering: yes, the idea is to present the end_date as last_day of the month+one. But it could easily be adapted.
It's not pretty, but I think it's somehow easy to understand.

compare oracle row count between different dates hourly

I am using this sql to query the count of rows hourly for three days ago ...
select trunc(sendtime ,'hh24') , count(*)
FROM t_sendedmsglog
where msgcontext like '%sm_%_tone_succ%' and sendtime > sysdate -3
group by trunc(sendtime ,'hh24')
order by trunc(sendtime ,'hh24') desc;
and the result shows like :
for example:
#|TRUNC(SENDTIME,'HH24')|COUNT(*)|
1|10/15/2020|12:00:00 PM|593|
2|10/15/2020|11:00:00 AM|889|
3|10/15/2020|10:00:00 AM|854|
4|10/15/2020|9:00:00 AM|1027|
5|10/15/2020|8:00:00 AM|8409|
.
.
.
12|10/15/2020|1:00:00 AM|101|
13|10/15/2020|281|
14|10/14/2020|11:00:00 PM|722|
15|10/14/2020|10:00:00 PM|1381|
16|10/14/2020|9:00:00 PM|2123|
.
.
25|10/14/2020|12:00:00 PM|1195|
26|10/14/2020|11:00:00 AM|1699|
27|10/14/2020|10:00:00 AM|747|
28|10/14/2020|9:00:00 AM|827|
.
.
40|10/13/2020|9:00:00 PM|2058|
41|10/13/2020|8:00:00 PM|2800|
but how I can make the result appear like below instead, so I can compare the count between different days for the same hour ?
hour|10/12/2020|10/13/2020|10/14/2020|count(*)
11:00:00 PM|618 |509 |722 |
10:00:00 PM|3181|1144|1381|
09:00:00 PM|3520|2058|2123|
08:00:00 PM|3688|2800|9347|
07:00:00 PM|3648|3166|3469|
06:00:00 PM|3628|2973|4518|
05:00:00 PM|3644|2429|3607|
04:00:00 PM|3652|3678|2291|
03:00:00 PM|1017|7711|819 |
02:00:00 PM|814 |7693|1310|
01:00:00 PM|856 |825 |848 |
12:00:00 PM|558 |1531|1195|
11:00:00 AM|0 |1132|1699|
10:00:00 AM|0 |732 |747 |
09:00:00 AM|0 |709 |827 |
08:00:00 AM|0 |1256|947 |
07:00:00 AM|0 |1465|1502|
06:00:00 AM|0 |749 |780 |
05:00:00 AM|0 |181 |169 |
04:00:00 AM|0 |46 |32 |
03:00:00 AM|0 |23 |34 |
02:00:00 AM|0 |46 |39 |
01:00:00 AM|0 |82 |81 |
00:00:00 AM|0 | |218 |
Use conditional aggregation:
select trunc(sendtime, 'hh24') , count(*) as total,
sum(case when trunc(sendtime) = trunc(sysdate) - interval '2' day then 1 else 0 end) as yester2day,
sum(case when trunc(sendtime) = trunc(sysdate) - interval '1' day then 1 else 0 end) as yesterday,
sum(case when trunc(sendtime) = trunc(sysdate) - interval '0' day then 1 else 0 end) as today
from t_sendedmsglog
where msgcontext like '%sm_%_tone_succ%' and
sendtime >= trunc(sysdate) - interval '2' day
group by trunc(sendtime, 'hh24')
order by trunc(sendtime, 'hh24') desc;
Note that I tweaked the date comparison in the where clause as well. In Oracle, sysdate has a time component, which you don't care about for the filtering purposes.

Resetting a date value if it falls on weekend

I am writing a procedure and using dynamic cursor and SQL query which I am passing as string in V_SQL variable. Query is as following:
In the where condition, I'm passing date but the condition is that if it's month end and it falls on Friday, Saturday or sunday then reset it to Thursday. for instance, 30th June will be sunday so value passed to day_of_month in SQL query should be 27 i.e. day no from Thursday date.
Could you please help me whether writing separate function will be good and what code should I put for better performance and desired result.
V_SQL := 'SELECT B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID, B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3, '||
'A.AS_OF_DATE, SUM(CURRENT_BAL) AS CB_SUM, SUM(AVG_BAL) AS AB_SUM, B.FLAG1 FROM DAILYGL A, AL_LOOKUP B '||
'WHERE A.GL_ACCOUNT_ID = B.GL_ACCT ***AND DAY_OF_MONTH = '|| TO_DO_FUNCTION(V_RUN_DATE)***
' AND ROWNUM <=15 GROUP BY B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID,B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3';
DAY_OF_MONTH = '|| TO_DO_FUNCTION(V_RUN_DATE)
Desired result will be passed Thursday day number if last business day is in fri, sat or sunday.
You don't really need a function, you can achieve this with a case expression, something like:
AND DAY_OF_MONTH = case
when last_day(v_run_date) - v_run_date <= 3
and to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
and to_char(last_day(v_run_date), 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
then next_day(trunc(v_run_date, 'IW'), 'THURSDAY')
else v_run_date
end
That is, if v_run_date is within three days of the last day of the month, and v_run_date is a Friday, Saturday or Sunday, and the last day of the month is a Friday, Saturday or Sunday, then use the date of that week's Thursday.
Demo with dates generated for all of this year:
with cte (v_run_date) as (
select date '2018-12-31' + level
from dual
connect by level <= 365
)
select v_run_date,
to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') as dy,
case
when last_day(v_run_date) - v_run_date <= 3
and to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
and to_char(last_day(v_run_date), 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
then next_day(trunc(v_run_date, 'IW'), 'THURSDAY')
else v_run_date
end as adjusted_date
from cte;
V_RUN_DATE DY ADJUSTED_D
---------- ------------ ----------
2019-01-01 Tue 2019-01-01
2019-01-02 Wed 2019-01-02
...
2019-01-30 Wed 2019-01-30
2019-01-31 Thu 2019-01-31
2019-02-01 Fri 2019-02-01
2019-02-02 Sat 2019-02-02
...
2019-03-26 Tue 2019-03-26
2019-03-27 Wed 2019-03-27
2019-03-28 Thu 2019-03-28
2019-03-29 Fri 2019-03-28
2019-03-30 Sat 2019-03-28
2019-03-31 Sun 2019-03-28
2019-04-01 Mon 2019-04-01
...
2019-04-30 Tue 2019-04-30
2019-05-01 Wed 2019-05-01
...
2019-05-28 Tue 2019-05-28
2019-05-29 Wed 2019-05-29
2019-05-30 Thu 2019-05-30
2019-05-31 Fri 2019-05-30
2019-06-01 Sat 2019-06-01
2019-06-02 Sun 2019-06-02
...
2019-06-26 Wed 2019-06-26
2019-06-27 Thu 2019-06-27
2019-06-28 Fri 2019-06-27
2019-06-29 Sat 2019-06-27
2019-06-30 Sun 2019-06-27
2019-07-01 Mon 2019-07-01
...
2019-07-31 Wed 2019-07-31
2019-08-01 Thu 2019-08-01
...
2019-08-28 Wed 2019-08-28
2019-08-29 Thu 2019-08-29
2019-08-30 Fri 2019-08-29
2019-08-31 Sat 2019-08-29
2019-09-01 Sun 2019-09-01
...
2019-11-27 Wed 2019-11-27
2019-11-28 Thu 2019-11-28
2019-11-29 Fri 2019-11-28
2019-11-30 Sat 2019-11-28
2019-12-01 Sun 2019-12-01
...
db<>fiddle
As mentioned in a comment, your code doesn't appear to need to be dynamic; you can use static SQL for what you've shown, e.g.:
SELECT *
-- into ...
FROM (
SELECT B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID,
B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3, A.AS_OF_DATE,
SUM(CURRENT_BAL) AS CB_SUM, SUM(AVG_BAL) AS AB_SUM, B.FLAG1
FROM DAILYGL A
JOIN AL_LOOKUP B ON B.GL_ACCT = A.GL_ACCOUNT_ID
WHERE DAY_OF_MONTH = case
when last_day(v_run_date) - v_run_date <= 3
and to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
and to_char(last_day(v_run_date), 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
then next_day(trunc(v_run_date, 'IW'), 'THURSDAY')
else v_run_date
end
GROUP BY B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID,B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3
-- order by something...
)
WHERE ROWNUM <= 15;
I've also switched to ANSI joins but more importantly moved the main query into an inline view, and then applied the rownum filter to that; which will make no difference unless/until you add an order-by clause inside that inline view. Without it this, and your original, will get an indeterminate (not quite random, but similar effect) set of 15 rows back. If you order the inner query then you'll always get the first 15 rows according to your ordering criteria. From 12c you can use a row limiting clause instead of the subquery/rownum filter, but this approach will work in earlier versions.
Obviously you still need to either select that into variables or use it as a cursor query - whatever you had planned to do dynamically.
I hope this will solve your problem
V_RUN_DATE - (case when V_RUN_DATE > LAST_DAY (V_RUN_DATE)- 3 then V_RUN_DATE else
(case TO_CHAR(date V_RUN_DATE, 'DY') when 'FRI' then 1 when 'SAT' then 2 when 'SUN' then 3 else 0 end) end)

Add target hours onto the next working day if job is logged after a certain time

Bit of a specific request / query but hope that I can explain it correctly, and that it makes sense.
The working day is 8am-5pm Monday-Friday
Each job has a target response time eg 1 hour, 2 hours, 4 hours
Some jobs it shows the target response time as outside of the working day eg a 4 hour job logged at 4:15pm will show a target response time of 8:15pm.
What I would like to do (and not even sure if it is possible) is:
If the priority_code is GC04 (1 hour job) and the time logged is after 4pm on a Monday-Fri take whatever time is before 5pm and add the remainder on to the next working day from 8am.
So an example would be 1 hour job logged at 4:15pm on Monday would show a target response time of 8:15am on Tuesday morning. (45 minutes used on Monday and 15 minutes carried over to Tuesday).
If the priority_code is GC05 (2 hour job) and the time logged is after 3pm on a Monday-Fri take whatever time is before 5pm and add the remainder on to the next working day from 8am.
So an example would be 2 hour job logged at 3:15pm on Monday would show a target response time of 8:15am on Tuesday morning. (1 hour 45 minutes used on Monday and 15 minutes carried over to Tuesday).
If the priority_code is GC06 (4 hour job) and the time logged is after 1pm on a Monday-Fri take whatever time is before 5pm and add the remainder on to the next working day from 8am.
So an example would be 4 hour job logged at 1:15pm on Monday would show a target response time of 8:15am on Tuesday morning. (3 hours 45 minutes used on Monday and 15 minutes carried over to Tuesday).
THANK YOU TO ALEX POOLE I'VE NOW GOT IT WORKING
Coding is below
select job_number, priority_code, job_entry_date, clock_start,
target_comp_date,
case
when to_char(target_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
and floor((target_time - trunc(target_time)) * 24) >= 17
then target_time + 2 + 63/24
when floor((target_time - trunc(target_time)) * 24) >= 17
then target_time + 15/24
else target_time
end as target_time
from (
select job_number, priority_code, job_entry_date, clock_start,
TARGET_COMP_DATE,
CASE
WHEN PRIORITY_CODE IN ('GC01','GC02','GC03','GC04','GC05','GC06','GC07')
THEN
clock_start
+ case priority_code
when 'GC01' then 1
when 'GC02' then 2
when 'GC03' then 0.5
when 'GC04' then 1
when 'GC05' then 2
when 'GC06' then 4
when 'GC07' then 24
end
/ 24
ELSE
TARGET_COMP_DATE END as target_time
from (
select job_number, priority_code, job_entry_date, target_comp_date,
case
when to_char(job_entry_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
and floor((job_entry_date - trunc(job_entry_date)) * 24) >= 17
then trunc(job_entry_date) + 80/24
when to_char(job_entry_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Sat'
then trunc(job_entry_date) + 56/24
when to_char(job_entry_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') =
'Sun'
or floor((job_entry_date - trunc(job_entry_date)) * 24) >= 17
then trunc(job_entry_date) + 32/24
when floor((job_entry_date - trunc(job_entry_date)) * 24) < 8
then trunc(job_entry_date) + 8/24
else job_entry_date
end as clock_start
from job
)
)
A slightly convoluted approach, which assumes your logged_time column is a timestamp (easy to adapt if it's a date), and that it can't be out-of-hours:
select id, priority_code, logged_time,
logged_time
+
-- response time
(
interval '1' hour
* case priority_code when 'GC04' then 1 when 'GC05' then 2 when 'GC06' then 4 end
)
+
-- actual time adjustment
(
-- possible time adjustment...
(
-- gap between 17:00 and 08:00
interval '15' hour
+
-- weekend days, only if Friday
(
interval '2' day
* case when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
then 1 else 0 end
)
)
*
-- ... but only if target exceeds 17:00
case when extract
(
hour from logged_time
+
-- response time
(
interval '1' hour
* case priority_code when 'GC04' then 1 when 'GC05' then 2 when 'GC06' then 4 end
)
) > 16 then 1 else 0 end
)
as target_time
from your_table;
which with some sample data like yours and just before your cut-offs, both on a Friday and Monday, gives:
ID PRIO LOGGED_TIME TARGET_TIME
---------- ---- --------------------- ---------------------
1 GC06 2019-05-26 12:59:59.0 2019-05-26 16:59:59.0
2 GC06 2019-05-26 13:15:00.0 2019-05-27 08:15:00.0
3 GC05 2019-05-26 14:59:59.0 2019-05-26 16:59:59.0
4 GC05 2019-05-26 15:15:00.0 2019-05-27 08:15:00.0
5 GC04 2019-05-26 15:59:59.0 2019-05-26 16:59:59.0
6 GC04 2019-05-26 16:15:00.0 2019-05-27 08:15:00.0
7 GC06 2019-05-31 12:59:59.0 2019-05-31 16:59:59.0
8 GC06 2019-05-31 13:15:00.0 2019-06-03 08:15:00.0
9 GC05 2019-05-31 14:59:59.0 2019-05-31 16:59:59.0
10 GC05 2019-05-31 15:15:00.0 2019-06-03 08:15:00.0
11 GC04 2019-05-31 15:59:59.0 2019-05-31 16:59:59.0
12 GC04 2019-05-31 16:15:00.0 2019-06-03 08:15:00.0
You can reduce some of the duplication with a CTE or inline view:
select id, priority_code, logged_time,
raw_target_time
+
-- actual time adjustment
(
-- possible time adjustment...
(
-- gap between 17:00 and 08:00
interval '15' hour
+
-- weekend days, only if Friday
(
interval '2' day
* case when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
then 1 else 0 end
)
)
*
-- ... but only if target exceeds 17:00
case when extract (hour from raw_target_time) > 16 then 1 else 0 end
)
as target_time
from (
select id, priority_code, logged_time,
logged_time
+
-- response time
(
interval '1' hour
* case priority_code when 'GC04' then 1 when 'GC05' then 2 when 'GC06' then 4 end
)
as raw_target_time
from your_table
);
and of course it doesn't need to be laid out like that, I was just trying to make the logic a bit clearer.
jobs can be logged at any time of the day using online web-forms, not just between 8am-5pm
This means that if a job is logged out-of-hours you need to treat it as if it was actually logged at the start of the next working day. (Note that I'm following your question in treating all Mon-Fri as working days - there is nothing in your question about public holidays for instance. Dealing with those would probably be a separate question.)
If you want to break it down you can first figure out when the clock starts on a given job, based on whether it was logged inside or outside the workday. There are a few ways to do this but since you have to deal with weekends I've chosen to do this as:
select id, priority_code, logged_time,
case
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
and floor((logged_time - trunc(logged_time)) * 24) >= 17
then trunc(logged_time) + 56/24
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Sat'
then trunc(logged_time) + 56/24
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Sun'
or floor((logged_time - trunc(logged_time)) * 24) >= 17
then trunc(logged_time) + 32/24
when floor((logged_time - trunc(logged_time)) * 24) < 8
then trunc(logged_time) + 8/24
else logged_time
end as clock_start
from your_table;
The floor((logged_time - trunc(logged_time)) * 24) gives you the hour that a job was logged, so you can see if that was lower than 8 (i.e. 8am) or greater than or equal to 17 (i.e. 5pm). Jobs logged at or after 17:00 on Friday or any time at weekends have their clock-start time pushed to the following Monday; jobs logged at or after 17:00 on other days are pushed to the following morning. That's using date arithmetic - 8/24 is 8 hours, 32/24 is 1 day and 8 hours, 56/24 is 2 days and 8 hours etc.
You can then put that into an inline view or CTE to simplify further calculations:
with cte1 (id, priority_code, logged_time, clock_start) as (
...
)
select id, priority_code, logged_time, clock_start,
clock_start
+ case priority_code when 'GC04' then 1 when 'GC05' then 2 when 'GC06' then 4 end
/ 24 as target_time
from cte1;
which will give you the basic target time; and then you can adjust that using similar logic to my earlier answer about timestamps and working-day-only logging, but this time using more date manipulation with fractional days instead of with intervals:
with cte1 (id, priority_code, logged_time, clock_start) as (
select id, priority_code, logged_time,
case
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
and floor((logged_time - trunc(logged_time)) * 24) >= 17
then trunc(logged_time) + 80/24
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Sat'
then trunc(logged_time) + 56/24
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Sun'
or floor((logged_time - trunc(logged_time)) * 24) >= 17
then trunc(logged_time) + 32/24
when floor((logged_time - trunc(logged_time)) * 24) < 8
then trunc(logged_time) + 8/24
else logged_time
end as clock_start
from your_table
),
cte2 (id, priority_code, logged_time, clock_start, target_time) as (
select id, priority_code, logged_time, clock_start,
clock_start
+ case priority_code when 'GC04' then 1 when 'GC05' then 2 when 'GC06' then 4 end
/ 24 as target_time
from cte1
)
select id, priority_code, logged_time, clock_start,
case
when to_char(target_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
and floor((target_time - trunc(target_time)) * 24) >= 17
then target_time + 63/24
when floor((target_time - trunc(target_time)) * 24) >= 17
then target_time + 15/24
else target_time
end as target_time
from cte2;
which with some made-up data for the scenarios I've thought about gives, with the days added just for info to make things hopefully slightly clearer:
ID PRIO LOGGED_TIME CLOCK_START TARGET_TIME LOGGED_DAY CLOCK_DAY TARGET_DAY
--- ---- ------------------- ------------------- ------------------- ---------- --------- ----------
1 GC06 2019-05-27 12:59:59 2019-05-27 12:59:59 2019-05-27 16:59:59 Mon Mon Mon
2 GC06 2019-05-27 13:15:00 2019-05-27 13:15:00 2019-05-28 08:15:00 Mon Mon Tue
3 GC05 2019-05-27 14:59:59 2019-05-27 14:59:59 2019-05-27 16:59:59 Mon Mon Mon
4 GC05 2019-05-27 15:15:00 2019-05-27 15:15:00 2019-05-28 08:15:00 Mon Mon Tue
5 GC04 2019-05-27 15:59:59 2019-05-27 15:59:59 2019-05-27 16:59:59 Mon Mon Mon
6 GC04 2019-05-27 16:15:00 2019-05-27 16:15:00 2019-05-28 08:15:00 Mon Mon Tue
7 GC04 2019-05-27 16:59:59 2019-05-27 16:59:59 2019-05-28 08:59:59 Mon Mon Tue
8 GC04 2019-05-27 17:00:00 2019-05-28 08:00:00 2019-05-28 09:00:00 Mon Tue Tue
9 GC04 2019-05-28 07:59:59 2019-05-28 08:00:00 2019-05-28 09:00:00 Tue Tue Tue
10 GC04 2019-05-28 08:00:00 2019-05-28 08:00:00 2019-05-28 09:00:00 Tue Tue Tue
11 GC06 2019-05-31 12:59:59 2019-05-31 12:59:59 2019-05-31 16:59:59 Fri Fri Fri
12 GC06 2019-05-31 13:15:00 2019-05-31 13:15:00 2019-06-03 08:15:00 Fri Fri Mon
13 GC05 2019-05-31 14:59:59 2019-05-31 14:59:59 2019-05-31 16:59:59 Fri Fri Fri
14 GC05 2019-05-31 15:15:00 2019-05-31 15:15:00 2019-06-03 08:15:00 Fri Fri Mon
15 GC04 2019-05-31 15:59:59 2019-05-31 15:59:59 2019-05-31 16:59:59 Fri Fri Fri
16 GC04 2019-05-31 16:15:00 2019-05-31 16:15:00 2019-06-03 08:15:00 Fri Fri Mon
17 GC04 2019-05-31 16:59:59 2019-05-31 16:59:59 2019-06-03 08:59:59 Fri Fri Mon
18 GC04 2019-05-31 17:00:00 2019-06-03 08:00:00 2019-06-03 09:00:00 Fri Mon Mon
19 GC04 2019-06-01 12:00:00 2019-06-03 08:00:00 2019-06-03 09:00:00 Sat Mon Mon
20 GC04 2019-06-02 12:00:00 2019-06-03 08:00:00 2019-06-03 09:00:00 Sun Mon Mon
21 GC04 2019-06-03 07:59:59 2019-06-03 08:00:00 2019-06-03 09:00:00 Mon Mon Mon
22 GC04 2019-06-03 08:00:00 2019-06-03 08:00:00 2019-06-03 09:00:00 Mon Mon Mon
db<>fiddle
Note that the CTE construct is providing aliases for the column expression from the with (...) clause, and that those do not have table aliases - as those aliases are unrelated to the tables inside the CTE. So it's with cte1 (id, priority_code, ... and not with cte1 (your_table.id, your_table.priority_code, ....
Also note that the semicolon at the end is a statement separator, required or optional (or configurable) in some clients, but invalid in others - it can cause ORA-00933 or ORA-00911 errors, and possibly others, in dynamic SQL, JDBC etc.; so ODBC probably also doesn't expect to see that final semicolon character.
If ODBC (or your version) doesn't allow CTEs - suggested by the 'not a SELECT statement' error you mentioned in a comment - then you can use inline views instead:
select id, priority_code, logged_time, clock_start,
case
when to_char(target_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
and floor((target_time - trunc(target_time)) * 24) >= 17
then target_time + 2 + 63/24
when floor((target_time - trunc(target_time)) * 24) >= 17
then target_time + 15/24
else target_time
end as target_time
from (
select id, priority_code, logged_time, clock_start,
clock_start
+ case priority_code when 'GC04' then 1 when 'GC05' then 2 when 'GC06' then 4 end
/ 24 as target_time
from (
select id, priority_code, logged_time,
case
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Fri'
and floor((logged_time - trunc(logged_time)) * 24) >= 17
then trunc(logged_time) + 80/24
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Sat'
then trunc(logged_time) + 56/24
when to_char(logged_time, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') = 'Sun'
or floor((logged_time - trunc(logged_time)) * 24) >= 17
then trunc(logged_time) + 32/24
when floor((logged_time - trunc(logged_time)) * 24) < 8
then trunc(logged_time) + 8/24
else logged_time
end as clock_start
from your_table
)
);
Added to db<>fiddle.
It's a little unclear but to keep a pre-calculated target_comp_date for other priorities you could change the first inline view (based on cte2) to have nested case expressions:
...
from (
select id, priority_code, logged_time, clock_start,
case when priority_code in ('GC04', 'GC05', 'GC06') then
-- for these, calculate the target time based on clock-start as before
clock_start
+ case priority_code when 'GC04' then 1 when 'GC05' then 2 when 'GC06' then 4 end
/ 24
else
-- for any other priority use the original pre-calculated time
target_comp_date
end as target_time
from (
select id, priority_code, logged_time, target_comp_date,
...
The innermost inline view needs to include that extra column in its select list, so it's visible to that nested case expression.
The system will show the target_comp_date incorrect as 12am
You should probably be fixing that existing code then, rather than trying to fudge the result it gives you.