Add X days to a Received Date but Exclude Weekends/Holidays from a Date Table - db2

I hope someone can help with a calculation that I am having trouble developing.
I am developing a report in a DB2 database that I need to add "X" number of days to a "RECEIVED" date/time when an order comes in between X and Y; but exclude Weekends and Holidays to add to the received date. I have created a [TBLCALENDAR] that lists the Weekends and Holidays (Example below); and from this, I want to ADD X number of days to a "DUEDATE"
tblCalendar]
DATE DAYOFWK DAY HOLIDAY
1/19/2019 7 Saturday
1/20/2019 1 Sunday
1/21/2019 2 Monday YES
So, for example 1, if I have an order that is placed on 1/18/2019 at 4:01pm; the due date should be 1/23/2019 at 11:00am.
Example 2: if I have an order that is placed on 1/18/2019 at
Conditions are:
Previous Date 4:01pm to Current Date 11:00am = Due Date should be + "X" business days by 11:00am
If order received Current day by 4:00pm = Due Date should be + "X" business days by 4:00pm
I have tried to reference the tblCalendar to get the [Received] date/time and add X number of days based off of an order, but it's not functioning the way I have hoped.
I have used the following code...but it doesn't exclude Weekends or Holidays when adding the specified number of days or have my order time requirement to take into account previous day after 4:00pm to current date of 11:00am:
RECEIVEDDATETIME + 2 days as DUEDATE;
I have also used the below code to reference TBLCALENDAR to find the # of holidays and weekend days in a date range:
( SELECT COUNT (*) FROM TBLCALENDAR AS C WHERE C.HOLIDAY = 'YES'
AND C.DATE BETWEEN TBLORDERS.RECEIVEDDATETIME
AND TBLORDERS.DUEDATETIME) +
(SELECT COUNT (*) FROM TBLCALENDAR
WHERE DAYOFWK IN (1,7)
AND DATE BETWEEN TBLORDERS.RECEIVEDDATETIME
AND TBLORDERS.UPLOADTIME) AS NONWORKINGDAYS
Expected field output
If order was received between 1/17/2019 4:01pm to 1/18/2019 10:59am = 1/23/2019 11:00am
If order received Current day by 4:00pm 1/18/2019 3:59am= 1/23/2019 by 4:00pm.
RECEIVEDDATETIME DUEDATE
1/17/2019 4:01pm 1/23/2019 11:00am
1/18/2019 10:00am 1/23/2019 4:00pm

Here is a solution without the time logic.
with tblCalendar(DATE, DAYOFWK, DAY, HOLIDAY) as (values
(date('2019-01-19'), 7, 'Saturday', '')
, (date('2019-01-20'), 1, 'Sunday', '')
, (date('2019-01-21'), 2, 'Monday', 'YES')
, (date('2019-01-22'), 3, 'Tuesday', '')
, (date('2019-01-23'), 4, 'Wednesday', 'YES')
, (date('2019-01-24'), 5, 'Thursday', '')
, (date('2019-01-25'), 6, 'Friday', '')
, (date('2019-01-26'), 7, 'Saturday', '')
)
, mytab (RECEIVEDDATE, DAYS2ADD) as (values
(date('2019-01-19'), 2)
, (date('2019-01-20'), 2)
, (date('2019-01-21'), 2)
, (date('2019-01-22'), 2)
)
select m.*, t.date as DUEDATE
--, dayofweek(date) as DAYOFWK, dayname(date) as DAY
from mytab m
, table
(
select date
from table
(
select
date
, sum(case when HOLIDAY='YES' or dayofweek(date) in (7,1) then 0 else 1 end) over (order by date) as dn_
from tblCalendar t
where t.date > m.RECEIVEDDATE
)
where dn_ = m.DAYS2ADD
fetch first 1 row only
) t;
The idea is to enumerate each day of the calendar after the RECEIVEDDATE (1-st parameter) starting from 1 with the following logic: the number of each day increases by 1 if it's non-holiday non-weekend day (the sum(...) over(...) expression).
Finally, we select a date with the corresponding number of days needed to add (2-nd parameter).

Solution idea:
Your tblCalendar is a good idea but I recommend to add the working day information instead of (only) flagging the holidays and weekends. The problem with the "off days" are that after you have figured out how many of them are in the period from your receive date to the receive date + X days you cannot easily add them because there could be other "off dates" in that perios again.
By numbering all the work days you could identify the workday which is closest (equal or bigger) to the receive date. Retrieve its number and add the X days to that number. Retrieve the date that has this work day number and you are fine.
The time logic should be built before that all because it could add another day to the X days.

Related

'3rd Friday of the Month' to a timestamp in PLPGSQL?

I have a database column giving me information on how often a file comes in.
Frequency_month
-------------
3rd Friday of the month
2nd Tuesday of the month
3rd Thursday of the month
I need to update this column and have it be a timestamp. e.g.
Frequency_month
-------------
2020-05-21 00:00:00
2020-05-11 00:00:00
2020-05-20 00:00:00
How can I accomplish this using postgres PLPGSQL language?
The following yields what your looking for. As far a parsing the Frequency_month it imposes the following restrictions:
The first character in the string is a digit indicating the relative
number.
This is followed 2 characters ordinal spec (st, nd, etc) and a space.
Actually any 3 characters, they are not checked.
Position 5 - 7 con the first 3 characters of the English day of week (dow).
If any of those are not satisfied you will need to change the S1 subquery.
Further it requires you to provide a date of reference. This may be any date in the month of interest. See comment by #sddk.
It proceeds as follows:
Parse the above extracting the week number, day of week, and last
day of the prior month. (S1).
Determine the ISODOW id numbers for the day of week specified and
DOW for last of prior month. (S2).
Using the ISODOW id numbers Determine, determine the first
occurrence of the target day in the target month. (S3).
Adjust the date from #3 by the additional weeks. (S4).
Finally, if the resulting date in #4 in still in the target month
return the date form #4. If it is not the same month then return
null. This occurs when there in no nth dow in the month or the dow
is incorrectly specified.
I have wrapped the above into a SQL function making parameterization easy. See Demo.
create or replace
function frequency_month( frequency_string text
, target_month date
)
returns date
language sql
as $$
with day_names( l_days) as
( values (array['mon','tue','wed','thu','fri','sat','sun']) )
select -- if the calculated date in still in the target month return that date else return null
-- covers invalid week in frequency 6th Friday or 0th Monday
case when extract(month from target_date) = extract (month from target_month)
then target_date
else null
end
from ( -- Advance from first dow in month the number of weeks to desirded dates
--select (first_of_mon + (7*(rel_num-1)) * interval '1 day')::date target_date
select (first_of_mon + (rel_num-1) * interval '1 week')::date target_date
from ( -- with last day of prior month get first DOW week of target month
select case when dow_day_nbr <= from_day_nbr
then (from_date + (dow_day_nbr-from_day_nbr+7) * interval '1 days' )::date
else (from_date + (dow_day_nbr-from_day_nbr) * interval '1 days' )::date
end first_of_mon
, rel_num
from ( -- Pick up ISODOW numbers
select array_position(l_days, (substring(to_char(from_date, 'day'),1,3))) as from_day_nbr
, array_position(l_days, lower(substring(rel_dow,1,3))) as dow_day_nbr
, from_date
, rel_num
from day_names
cross join ( -- get last day of prior month, desired relative day, relative dow
select substr(frequency_string,1,1)::integer rel_num
, lower(substr(frequency_string,5,3)) rel_dow
, (date_trunc('month',target_month) - interval '1 day')::date from_date
) s1
) s2
) s3
) s4;
$$;
Note: The demo also includes a standalone version if a function is not desired.

Select a custom date range period to retrieve results from last month day

I have a lot of data with a lot of dates (date begin, date end, date activation etc.). I would like to retrieve those data selecting a specific time range and returning a period date.
I want only results where:
(last month date) <= Date Activation and (last month date) > Date End
+ return the column containing the period
If I select a unique period:
select "client Name","Program" from "database"."schema"."table"
WHERE "Date Activation" <= '2020-12-31' AND "Date End" > '2020-12-31'
The aim is to retrieve results like this (I want it for all periods in my table):
client Name
Program
period
client 1
program 1
2020/11/30
client 2
program 2
2020/12/31
client 3
program 3
2020/12/31
client 3
program 3
2021/01/31
client 1
program 1
2021/01/31
client 2
program 4
2021/02/28
This should achieve what you want:
-- set parameter to be used as generator "constant" including the start day
-- set start and end dates to match the date range you want to report on
set num_days = (Select datediff(day, TO_DATE('2020-01-01','YYYY-MM-DD'), current_date()+1));
-- generate all the dates between the start and end dates
with date_list as (
select
dateadd(
day,
'-' || row_number() over (order by null),
dateadd(day, '+1', current_date())
) as date_val
from table (generator(rowcount => ($num_days)))
),
-- create a distinct list of month-end dates from the list of dates
month_list as (
select distinct last_day(date_val) as month_end
from date_list
)
-- Join the list of month-end dates to your data table
select
cpd.client_name
,cpd.program
,ml.month_end
from month_list ml
inner join client_project_data cpd on cpd.Date_Activation <= ml.month_end and cpd.Date_End > ml.month_end;
-- clean up previously set variable
-- unset num_days;
I believe this is how you would get the first two columns:
SELECT DISTINCT "client Name", "Program"
FROM "database"."schema"."table"
WHERE "Date Activation" < "Date End" AND LAST_DAY("Date Activation") <> LAST_DAY("Date End")
But with the third one you will have to get creative.
If the difference between "Date Activation" and "Date End" can only be one month, then LAST_DAY("Date Activation") would do it.
But if the difference is bigger, then you will probably need two or more month ends to list. You should form some kind of array of month ends which are between "Date Activation" and "Date End". And the you would need to make separate rows out of such array.

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

Assessing the closest future date to today from 4 columns with dates in SQL

I have a table which features 4 columns of dates. I need to calculate in column 5 the date which is the closest future date to today and display this within the same row e.g. is the 20th anniv closer that the 85th birthday or is theh 10th anniv closer than the 85th birthday. NB the 85th birthday will alway be the maximum date. Column 6 needs to then display the appriopriate column heading
Really appreciate any help that any one can offer.
Column names / sample values
strt date - 01/01/2010
85th birthday - 11/12/2047
10th anniv - 01/01/2020
20th anniv - 01/01/2030
next date - 01/01/2030
anniv_type - 20th anniv
The following query uses CROSS APPLY to UNPIVOT the 4 date columns and then get the one closest to current date. TOP 1 is to get only one row ie the nearest date
select *
from yourtable t
cross apply
(
select top 1 *
from
(
values ([strt date], 'strt date'),
([85th birthday], '85th birthday'),
([10th anniv], '10th anniv'),
([20th anniv], '20th anniv')
) d ([next date], [anniv_type])
where [next date] > getdate()
order by datediff(day, getdate(), [next date])
) n

Counting by Week in Hive

I'm trying to produce a fully refreshed set of numbers each week, pulling from a table in hive. Right now I using this method:
SELECT
COUNT(DISTINCT case when timestamp between TO_DATE("2016-01-28") and TO_DATE("2016-01-30") then userid end) as week_1,
COUNT(DISTINCT case when timestamp between TO_DATE("2016-01-28") and TO_DATE("2016-02-06") then userid end) as week_2
FROM Data;
I'm trying to get something more along the lines of:
SELECT
Month(timestamp), Week(timestamp), COUNT (DISTINCT userid)
FROM Data
Group By Month, Week
But my week runs Sunday to Saturday. Is there a smarter way to be doing this that works in HIVE?
Solution found:
You can simply create your own formula instead of going with pre-defined function for "week of the year" Advantage: you will be able to take any set of 7 days for a week.
In your case since you want the week should start from Sunday-Saturday we will just need the first date of sunday in a year
eg- In 2016, First Sunday is on '2016-01-03' which is 3rd of Jan'16 --assumption considering the timestamp column in the format 'yyyy-mm-dd'
SELECT
count(distinct UserId), lower(datediff(timestamp,'2016-01-03') / 7) + 1 as week_of_the_year
FROM table.data
where timestamp>='2016-01-03'
group by lower(datediff(timestamp,'2016-01-03') / 7) + 1;
I see that you need the data to be grouped by week. you can just do this :
SELECT weekofyear(to_date(timestamp)), COUNT (DISTINCT userid) FROM Data Group By weekofyear(to_date(timestamp))