Exclude weekend between two timestamps in a query in Db2 - db2

I have two columns with a timestamp in each
column_a column_b
2021-08-03 13:22:29 2021-08-09 15:51:59
I want to calculate the difference in hours, but exclude the weekend (if the dates fall on or between the two timestamps).
I have tried TIMESTAMPDIFF and HOURS_BETWEEN - but these would still include the weekend.
UPDATE:
my solution was to ...
create a function to calculate the number of days between the two days, excluding weekends taken from here
How to get the WORKING day diff in db2 without saturdays and sundays?
Then in my SELECT used Db2s native DATEDIFF(8,xxx,yyy) to get the total number of hours, and subtracted from this DATEDIFF, the value returned from the function * 24 (for hours)

WITH
MYTAB (A, B) AS
(
VALUES ('2021-08-03 13:22:29'::TIMESTAMP, '2021-08-09 15:51:59'::TIMESTAMP)
)
, MYTAB2 (A, B) AS
(
SELECT
CASE WHEN DAYOFWEEK(A) IN (1, 7) THEN DATE (A) + 1 DAY ELSE A END
, CASE WHEN DAYOFWEEK(B) IN (1, 7) THEN B - (MIDNIGHT_SECONDS (B) + 1) SECOND ELSE B END
FROM MYTAB
)
, R (TS) AS
(
SELECT V.TS
FROM MYTAB2 T, TABLE (VALUES T.A, T.B) V (TS)
WHERE T.A <= T.B
UNION ALL
SELECT DATE (R.TS) + 1 DAY
FROM R, MYTAB2 T
WHERE DATE (R.TS) + 1 DAY < DATE (T.B)
)
SELECT
COALESCE
(
-- Seconds between start and end
(DAYS (MAX (TS)) - DAYS (MIN (TS))) * 86400
+ MIDNIGHT_SECONDS (MAX (TS)) - MIDNIGHT_SECONDS (MIN (TS))
-- SUM of seconds for weekend days is subtracted
- SUM (CASE WHEN DAYOFWEEK (TS) IN (1, 7) THEN 86400 ELSE 0 END)
, 0
) / 3600 AS HRS
FROM R
The idea is to construct the following table with Recursive Common Table expression first:
2021-08-03 13:22:29 --> 2021-08-04 00:00:00 (if start is a weekend)
2021-08-04 00:00:00 --> 2021-08-05 00:00:00 (if start is a weekend)
...
2021-08-08 00:00:00 --> 2021-08-07 00:00:00 (if end is a weekend)
2021-08-09 15:51:59 --> 2021-08-08 23:59:59 (if end is a weekend)
That is: one timestamp for each day between the start and the end timestamps. These start and end timestamps are adjusted:
If start is a weekend - change it to the start of the next day
If end is a weekend - change it to the end of the previous day
The final calculation is simple: we subtract sum of seconds for all weekends in the list from the difference in seconds between start and end.

Related

How to get financial year wise periods for a given date range

My financial year start from 01-Jul to 30-Jun every year.
I want to find out all financial year wise periods for a given date range.
Let's say, The date range is From_Date:16-Jun-2021 To_Date 31-Aug-2022. Then my output should be like
Start_Date, End_date
16-Jun-2021, 30-Jun-2021
01-Jul-2021, 30-Jun-2022
01-jul-2022, 31-Aug-2022
Please help me query. First record Start_Date must start from From_Date and Last record End_Date must end at To_Date
This should work for the current century.
with t(fys, fye) as
(
select (y + interval '6 months')::date,
(y + interval '1 year 6 months - 1 day')::date
from generate_series ('2000-01-01'::date, '2100-01-01', interval '1 year') y
),
periods (period_start, period_end) as
(
select
case when fys < '16-Jun-2021'::date then '16-Jun-2021'::date else fys end,
case when fye > '31-Aug-2022'::date then '31-Aug-2022'::date else fye end
from t
)
select * from periods where period_start < period_end;
period_start
period_end
2021-06-16
2021-06-30
2021-07-01
2022-06-30
2022-07-01
2022-08-31
Looks well as a parameterized query too with '16-Jun-2021' and '31-Aug-2022' replaced by parameter placeholders.
You want to create multiple records from one record (your date range). To accomplish this, you will need some kind of helper table.
In this example I created that helper table using GENERATE_SERIES and use it to join it to your date range, with some logic to get the dates you want.
dbfiddle
--Generate a range of fiscal years
WITH FISCAL_YEARS AS (
SELECT
CONCAT(SEQUENCE.YEAR, '-07-01')::DATE AS FISCAL_START,
CONCAT(SEQUENCE.YEAR + 1, '-06-30')::DATE AS FISCAL_END
FROM GENERATE_SERIES(2000, 2030) AS SEQUENCE (YEAR)
),
--Your date range
DATE_RANGE AS (
SELECT
'2021-06-16'::DATE AS RANGE_START,
'2022-08-31'::DATE AS RANGE_END
)
SELECT
--Case statement in case the range_start is later
--than the start of the fiscal year
CASE
WHEN RANGE_START > FISCAL_START
THEN RANGE_START
ELSE FISCAL_START
END AS START_DATE,
--Case statement in case the range_end is earlier
--than the end of the fiscal year
CASE
WHEN RANGE_END < FISCAL_END
THEN RANGE_END
ELSE FISCAL_END
END AS END_DATE
FROM FISCAL_YEARS
JOIN DATE_RANGE
--Join to get all relevant fiscal years
ON FISCAL_YEARS.FISCAL_START BETWEEN DATE_RANGE.RANGE_START AND DATE_RANGE.RANGE_END
OR FISCAL_YEARS.FISCAL_END BETWEEN DATE_RANGE.RANGE_START AND DATE_RANGE.RANGE_END

To get weekday between two date in Postgresql

I need to get all week days in a given time interval.
In postgresql, there are dow and isodow
By mixing them together, may I write a function to retrieve weekdays?
demo:db<>fiddle
SELECT
generated_date,
to_char(generated_date, 'Day'), -- 1
EXTRACT(isodow FROM generated_date), -- 2
EXTRACT(dow FROM generated_date) -- 3
FROM
generate_series('2020-11-01'::date, '2020-11-10'::date, interval '1 day') AS generated_date
Returns the name for the weekday
Returns the number of the weekday (Monday = 1, Sunday = 7)
Returns the number of the weekday (Sunday = 0, Saturday = 6)
Edit:
If you want to get the days without weekend, you can filter by the dow/isodow values, e.g.:
SELECT
generated_date::date
FROM
generate_series('2020-11-01'::date, '2020-11-10'::date, interval '1 day') AS generated_date
WHERE
EXTRACT(isodow FROM generated_date) < 6
As far as I understand you need to extract all Monday..Fridays between two dates. Here is an illustration with 2020-11-30 as the beginning of the interval and 2020-12-12 as the end of it.
select d
from generate_series('2020-11-30'::date, '2020-12-12'::date, '1 day'::interval) t(d)
where extract(isodow from d) between 1 and 5;

DB2: Bi-monthly query for a DB2 report

I am currently writing a Crystal Report that has a DB2 query as its backend. I have finished the query but am stuck on the date portion of it. I am going to be running it twice a month - once on the 16th, and once on the 1st of the next month. Here's how it should work:
If I run it on the 16th of the month, it will give me results from the 1st of that same month to the 15th of that month.
If I run it on the 1st of the next month, it will give me results from the 16th of the previous month to the last day of the previous month.
This comes down a basic bi-monthly report. I've found plenty of hints to do this in T-SQL, but no efficient ways on how to accomplish this in DB2. I'm having a hard time wrapping my head around the logic to get this to consistently work, taking into account differences in month lengths and such.
There are 2 expressions for start and end date of an interval depending on the report date passed, which you may use in your where clause.
The logic is as follows:
1) If the report date is the 1-st day of a month, then:
DATE_START is 16-th of the previous month
DATE_END is the last day of the previous month
2) Otherwise:
DATE_START is 1-st of the current month
DATE_END is 15-th of the current month
SELECT
REPORT_DATE
, CASE DAY(REPORT_DATE) WHEN 1 THEN REPORT_DATE - 1 MONTH + 15 ELSE REPORT_DATE - DAY(REPORT_DATE) + 1 END AS DATE_START
, CASE DAY(REPORT_DATE) WHEN 1 THEN REPORT_DATE - 1 ELSE REPORT_DATE - DAY(REPORT_DATE) + 15 END AS DATE_END
FROM
(
VALUES
DATE('2020-02-01')
, DATE('2020-02-05')
, DATE('2020-02-16')
) T (REPORT_DATE);
The result is:
|REPORT_DATE|DATE_START|DATE_END |
|-----------|----------|----------|
|2020-02-01 |2020-01-16|2020-01-31|
|2020-02-05 |2020-02-01|2020-02-15|
|2020-02-16 |2020-02-01|2020-02-15|
In Db2 (for Unix, Linux and Windows) it could be a WHERE Condition like
WHERE
(CASE WHEN date_part('days', CURRENT date) > 15 THEN yourdatecolum >= this_month(CURRENT date) AND yourdatecolum < this_month(CURRENT date) + 15 days
ELSE yourdatecolum > this_month(CURRENT date) - 1 month + 15 DAYS AND yourdatecolum < this_month(CURRENT date)
END)
Check out the THIS_MONTH function - there are multiple ways to do it. Also DAYS_TO_END_OF_MONTH might be helpful

generate series based on particular day in each month -postgresql

i have following query in postgresql for dates between 2 ranges.
select generate_series('2019-04-01'::timestamp, '2020-03-31', '1 month')
as g_date
I need to generate specific date in every month .i.e 15 th of every month. Following is my query to generate series
DO $$
DECLARE
compdate date = '2019-04-15';
BEGIN
CREATE TEMP TABLE tmp_table ON COMMIT DROP AS
select *,
case
when extract('day' from d) <> extract('day' from compdate) then 0
when ( extract('month' from d)::int - extract('month' from compdate)::int ) % 1 = 0 then 1
else 0
end as c
from generate_series('2019-04-01'::timestamp, '2020-03-31', '1 day') d;
END $$;
SELECT * FROM tmp_table
where c=1;
;
But every thing is perfect if input date between (1..29)-04-2019 ..
2019-04-25
2019-05-25
2019-06-25
2019-07-25
2019-08-25
2019-09-25
2019-10-25
2019-11-25
2019-12-25
2020-01-25
2020-02-25
2020-03-25
but if i give compdate: 31-04-2019 or 30-04-2019 giving out put:
2019-05-31
2019-07-31
2019-08-31
2019-10-31
2019-12-31
2020-01-31
2020-03-31
Expected Output:
date flag
2019-04-01 0 ----start_date
2019-04-30 1
2019-05-31 1
2019-06-30 1
2019-07-31 1
2019-08-31 1
2019-09-30 1
2019-10-31 1
2019-11-30 1
2019-12-31 1
2020-01-31 1
2020-02-29 1
2020-03-31 0 ---end_date
If matched day not found in the result it should take last day of that month..i.e if 31 not found in month of feb it
should take 29-02-2019 and also in april month instead of 31 it should take 2019-04-30.
Please suggest.
to generate the last days of the month, just generate first days & subtract a 1 day interval
example: the following generates all last day of month in the year 2010
SELECT x - interval '1 day' FROM
GENERATE_SERIES('2010-02-01', '2011-01-01', interval '1 month') x
You cannot accomplish what you want with generate_series. This results due to that process applying a fixed increment from the previous generated value. Your case 1 month. Now Postgres will successfully compute correct end-of-month date from 1 month to the next. So for example 1month from 31-Jan yields 28-Feb (or 29), because 31-Feb would be an invalid date, Postgres handles it. However, that same interval from 28-Feb gives the valid date 28-Mar so no end-of-month adjustment is needed. Generate_Series will return 28th of the month from then on. The same applies to 30 vs. 31 day months.
But you can achieve what your after with a recursive CTE by employing a varying interval to the same initial start date. If the resulting date is invalid for date the necessary end-of-month adjustment will be made. The following does that:
create or replace function constant_monthly_date
( start_date timestamp
, end_date timestamp
)
returns setof date
language sql strict
as $$
with recursive date_set as
(select start_date ds, start_date sd, end_date ed, 1 cnt
union all
select (sd + cnt*interval '1 month') ds, sd, ed, cnt+1
from date_set
where ds<end_date
)
select ds::date from date_set;
$$;
-- test
select * from constant_monthly_date(date '2020-01-15', date '2020-12-15' );
select * from constant_monthly_date(date '2020-01-31', date '2020-12-31' );
Use the least function to get the least one between the computed day and end of month.
create or replace function test1(day int) returns table (t timestamptz) as $$
select least(date_trunc('day', t) + make_interval(days => day-1), date_trunc('day', t) + interval '1 month' - interval '1 day') from generate_series('2019-04-01', '2020-03-31', interval '1 month') t
$$ language sql;
select test1(31);

postgres: generate series of timestamps respecting time zone

I'm stumped by a tricky issue regarding time zone changes from daylight savings to non daylight savings.
I'm trying to generate a series of timestamps, 6 hrs apart. This is later joined with data with corresponding timestamps at the 00, 06, 12, 18 hrs for each day in the dataset.
This works fine normally, using:
generate_series(extract(epoch from start_ts)::integer, extract(epoch from end_ts)::integer, 21600)
where start_ts is 00 hr on the first date, and end_ts is 00 hr on the last date exclusive.
However, when timezone offset goes from +11 to +10 half way through the series, it will no longer match any records since the series elements become 1 hr off.
Does anyone have suggestions on how to generate a series of 'epoch integers' or timestamps which would match 00,06,12,18 hr timestamps while respecting the timezone's offset?
This will generate it (using PostgreSQL 9.5+), starting from today and for 10 days:
select (current_date::timestamp + ((a-1)||' days')::interval)::timestamptz
from generate_series(1, 10, .25) a
Test it on a whole year:
select *, date_part('hour', d::timestamp), d::timestamp
from (
select (current_date::timestamp + ((a-1)||' days')::interval)::timestamptz AS d
from generate_series(1, 365, .25) a
) x
where date_part('hour', d) not in (0, 6, 12, 18)
Edit: The version below works with versions of PostgreSQL older than 9.5:
select (current_date::timestamp + (((a-1)/4.0)||' days')::interval)::timestamptz
from generate_series(1, 4* 10 ) a -- 10 days
#Ziggy's answer is great, use that. however here's how I solved it in my application which can't use decimals in generate_series (v9.4):
_min timestamp with time zone, -- the first timestamp in the series
_max timestamp with time zone, -- the last timestamp in the series
_inc integer, -- the increment in seconds, eg 21600 (6hr)
_tz text
creates a series from the _max down using the tz offset of the _max,
creates a series from the _min up using the tz offset of the _min,
merges the results
validates each result is divisible by the _inc in the tz of the result, discards if not
query:
select t1 from (
select ser,
to_timestamp(ser) t1,
extract(epoch from
to_timestamp(ser) at time zone _tz
- date_trunc('day', to_timestamp(ser) at time zone _tz)
)::integer % _inc = 0 is_good
from (
select 'ser1' s, generate_series(extract(epoch from _min)::integer, extract(epoch from _max)::integer, _inc) ser
union all
select 'ser2' s, generate_series(extract(epoch from _max)::integer, extract(epoch from _min)::integer, _inc * -1) ser
) x
group by ser, _tz, _inc
order by ser asc
) x
where is_good
;