Extract 5th Business Date from sysdate - sql

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

Related

CASE in WHERE Clause in Snowflake

I am trying to do a case statement within the where clause in snowflake but I’m not quite sure how should I go about doing it.
What I’m trying to do is, if my current month is Jan, then the where clause for date is between start of previous year and today. If not, the where clause for date would be between start of current year and today.
WHERE
CASE MONTH(CURRENT_DATE()) = 1 THEN DATE BETWEEN DATE_TRUNC(‘YEAR’, DATEADD(YEAR, -1, CURRENT_DATE())) AND CURRENT_DATE()
CASE MONTH(CURRENT_DATE()) != 1 THEN DATE BETWEEN DATE_TRUNC(‘YEAR’, CURRENT_DATE()) AND CURRENT_DATE()
END
Appreciate any help on this!
Use a CASE expression that returns -1 if the current month is January or 0 for any other month, so that you can get with DATEADD() a date of the previous or the current year to use in DATE_TRUNC():
WHERE DATE BETWEEN
DATE_TRUNC('YEAR', DATEADD(YEAR, CASE WHEN MONTH(CURRENT_DATE()) = 1 THEN -1 ELSE 0 END, CURRENT_DATE()))
AND
CURRENT_DATE()
I suspect that you don't even need to use CASE here:
WHERE
(MONTH(CURRENT_DATE()) = 1 AND
DATE BETWEEN DATE_TRUNC(‘YEAR’, DATEADD(YEAR, -1, CURRENT_DATE())) AND
CURRENT_DATE()) OR
(MONTH(CURRENT_DATE()) != 1 AND
DATE BETWEEN DATE_TRUNC(‘YEAR’, CURRENT_DATE()) AND CURRENT_DATE())
So the other answers are quite good, but... the answer can be even simpler
Making a little table to brake down what is happening.
select
row_number() over (order by null) - 1 as rn,
dateadd('day', rn * 5, date_trunc('year',current_date())) as pretend_current_date,
DATEADD(YEAR, -1, pretend_current_date) as pcd_sub1,
month(pretend_current_date) as pcd_month,
DATE_TRUNC(year, iff(pcd_month = 1, pcd_sub1, pretend_current_date)) as _from,
pretend_current_date as _to
from table(generator(ROWCOUNT => 30))
order by rn;
this shows:
RN
PRETEND_CURRENT_DATE
PCD_SUB1
PCD_MONTH
_FROM
_TO
0
2022-01-01
2021-01-01
1
2021-01-01
2022-01-01
1
2022-01-06
2021-01-06
1
2021-01-01
2022-01-06
2
2022-01-11
2021-01-11
1
2021-01-01
2022-01-11
3
2022-01-16
2021-01-16
1
2021-01-01
2022-01-16
4
2022-01-21
2021-01-21
1
2021-01-01
2022-01-21
5
2022-01-26
2021-01-26
1
2021-01-01
2022-01-26
6
2022-01-31
2021-01-31
1
2021-01-01
2022-01-31
7
2022-02-05
2021-02-05
2
2022-01-01
2022-02-05
8
2022-02-10
2021-02-10
2
2022-01-01
2022-02-10
9
2022-02-15
2021-02-15
2
2022-01-01
2022-02-15
10
2022-02-20
2021-02-20
2
2022-01-01
2022-02-20
11
2022-02-25
2021-02-25
2
2022-01-01
2022-02-25
12
2022-03-02
2021-03-02
3
2022-01-01
2022-03-02
13
2022-03-07
2021-03-07
3
2022-01-01
2022-03-07
14
2022-03-12
2021-03-12
3
2022-01-01
2022-03-12
15
2022-03-17
2021-03-17
3
2022-01-01
2022-03-17
16
2022-03-22
2021-03-22
3
2022-01-01
2022-03-22
17
2022-03-27
2021-03-27
3
2022-01-01
2022-03-27
18
2022-04-01
2021-04-01
4
2022-01-01
2022-04-01
19
2022-04-06
2021-04-06
4
2022-01-01
2022-04-06
20
2022-04-11
2021-04-11
4
2022-01-01
2022-04-11
21
2022-04-16
2021-04-16
4
2022-01-01
2022-04-16
22
2022-04-21
2021-04-21
4
2022-01-01
2022-04-21
23
2022-04-26
2021-04-26
4
2022-01-01
2022-04-26
24
2022-05-01
2021-05-01
5
2022-01-01
2022-05-01
25
2022-05-06
2021-05-06
5
2022-01-01
2022-05-06
26
2022-05-11
2021-05-11
5
2022-01-01
2022-05-11
27
2022-05-16
2021-05-16
5
2022-01-01
2022-05-16
28
2022-05-21
2021-05-21
5
2022-01-01
2022-05-21
29
2022-05-26
2021-05-26
5
2022-01-01
2022-05-26
Your logic is asking "is the current date in the month of January", at which point take the prior year, and then date truncate to the year, otherwise take the current date and truncate to the year. As the start of a BETWEEN test.
This is the same as getting the current date subtracting one month, and truncating this to year.
Thus there is no need for any IFF or CASE
WHERE date BETWEEN DATE_TRUNC(year, DATEADD(month,-1, CURRENT_DATE())) AND CURRENT_DATE()
and if you like to drop some paren's, CURRENT_DATE can be used if you leave it in upper case, thus it can even be smaller:
WHERE date BETWEEN DATE_TRUNC(year, DATEADD(month,-1, CURRENT_DATE)) AND CURRENT_DATE

Oracle get last weekday Mon-Fri

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

Creating a Time Dimension table with Intervals in Oracle

What is the easiest way to create a table in oracle to get the output as shown below;
Time Key
5 Minute Interval
10 Minute Interval
15 Minute Interval
100
12:00:00
12:10:00
12:15:00
101
12:05:00
12:10:00
12:15:00
102
12:10:00
12:20:00
12:15:00
103
12:15:00
12:20:00
12:30:00
The hours will be displayed in 24 hour format and will be used to join the time key onto a fact table to be loaded into a Microsoft Analysis Tabular Model.
Seems that you need a row generator. Here's an example.
Date format (just to know what you're looking at):
SQL> alter session set nls_date_format = 'dd.mm.yyyy hh24:mi:ss';
Session altered.
Query (creates 10 rows of data; change value in line #9 for more (or less) rows):
SQL> with temp (time_key, datum) as
2 (select 99, trunc(sysdate) datum from dual)
3 select
4 time_key + level time_key,
5 datum + ( 5 * (level - 1)) / (24 * 60) five_min,
6 datum + (10 * (level - 1)) / (24 * 60) ten_min,
7 datum + (15 * (level - 1)) / (24 * 60) fifteen_min
8 from temp
9 connect by level <= 10;
TIME_KEY FIVE_MIN TEN_MIN FIFTEEN_MIN
---------- ------------------- ------------------- -------------------
100 04.01.2021 00:00:00 04.01.2021 00:00:00 04.01.2021 00:00:00
101 04.01.2021 00:05:00 04.01.2021 00:10:00 04.01.2021 00:15:00
102 04.01.2021 00:10:00 04.01.2021 00:20:00 04.01.2021 00:30:00
103 04.01.2021 00:15:00 04.01.2021 00:30:00 04.01.2021 00:45:00
104 04.01.2021 00:20:00 04.01.2021 00:40:00 04.01.2021 01:00:00
105 04.01.2021 00:25:00 04.01.2021 00:50:00 04.01.2021 01:15:00
106 04.01.2021 00:30:00 04.01.2021 01:00:00 04.01.2021 01:30:00
107 04.01.2021 00:35:00 04.01.2021 01:10:00 04.01.2021 01:45:00
108 04.01.2021 00:40:00 04.01.2021 01:20:00 04.01.2021 02:00:00
109 04.01.2021 00:45:00 04.01.2021 01:30:00 04.01.2021 02:15:00
10 rows selected.
SQL>
In Oracle, there's no "time" datatype. We use date and it consists of both date AND time. Your example shows that you want time only; well, you can't have it, not as a date datatype. You can use e.g. TO_CHAR function on it (and fetch only time component). I wouldn't recommend you to store those values as strings into varchar2 datatype column as nothing prevents you (or someone else) to put e.g. 12:3f:75 into it, and that certainly isn't valid time value.
So, you'd then:
SQL> create table test as
2 with temp (time_key, datum) as
3 (select 99, trunc(sysdate) datum from dual)
4 select
5 time_key + level time_key,
6 datum + ( 5 * (level - 1)) / (24 * 60) five_min,
7 datum + (10 * (level - 1)) / (24 * 60) ten_min,
8 datum + (15 * (level - 1)) / (24 * 60) fifteen_min
9 from temp
10 connect by level <= 10;
Table created.
SQL> select time_key,
2 to_char(five_min, 'hh24:mi:ss') five_min
3 from test;
TIME_KEY FIVE_MIN
---------- --------
100 00:00:00
101 00:05:00
102 00:10:00
103 00:15:00
104 00:20:00
105 00:25:00
106 00:30:00
107 00:35:00
108 00:40:00
109 00:45:00
10 rows selected.
SQL>
To answer question T. Peter posted as a comment: I don't know for other databases, but - principle that should work elsewhere is to use a cross join (Cartesian product) with a subquery that returns "a lot of rows". In Oracle, ALL_OBJECTS is such a table. It isn't indefinite; in my sample schema, it contains ~8000 rows. For example (see lines #8 - 10):
SQL> with temp (time_key, datum) as
2 (select 99, trunc(sysdate) datum from dual)
3 select
4 time_key + rn time_key,
5 datum + ( 5 * (rn - 1)) / (24 * 60) five_min,
6 datum + (10 * (rn - 1)) / (24 * 60) ten_min,
7 datum + (15 * (rn - 1)) / (24 * 60) fifteen_min
8 from temp cross join (select rownum rn
9 from all_tables
10 where rownum <= 10
11 );
TIME_KEY FIVE_MIN TEN_MIN FIFTEEN_MIN
---------- ------------------- ------------------- -------------------
100 04.01.2021 00:00:00 04.01.2021 00:00:00 04.01.2021 00:00:00
101 04.01.2021 00:05:00 04.01.2021 00:10:00 04.01.2021 00:15:00
<snip>
Basically, keyword here is "row generator". I suggest you Google for it and add database name you use.
You can use a recursive sub-query factoring clause and get the time values using INTERVAL DAY TO SECOND data type:
CREATE TABLE table_name ( time_key, five_minute, ten_minute, fifteen_minute ) AS
WITH data ( time_key, five_minute, ten_minute, fifteen_minute, num_rows ) AS (
SELECT 100,
INTERVAL '12:00' HOUR TO MINUTE,
INTERVAL '12:00' HOUR TO MINUTE,
INTERVAL '12:00' HOUR TO MINUTE,
4
FROM DUAL
UNION ALL
SELECT time_key + 1,
five_minute + INTERVAL '5' MINUTE,
ten_minute + INTERVAL '10' MINUTE,
fifteen_minute + INTERVAL '15' MINUTE,
num_rows - 1
FROM data
WHERE num_rows > 1
)
SELECT time_key,
five_minute,
ten_minute,
fifteen_minute
FROM data;
Then the table contains:
TIME_KEY | FIVE_MINUTE | TEN_MINUTE | FIFTEEN_MINUTE
-------: | :---------------------------- | :---------------------------- | :----------------------------
100 | +000000000 12:00:00.000000000 | +000000000 12:00:00.000000000 | +000000000 12:00:00.000000000
101 | +000000000 12:05:00.000000000 | +000000000 12:10:00.000000000 | +000000000 12:15:00.000000000
102 | +000000000 12:10:00.000000000 | +000000000 12:20:00.000000000 | +000000000 12:30:00.000000000
103 | +000000000 12:15:00.000000000 | +000000000 12:30:00.000000000 | +000000000 12:45:00.000000000
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.

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.