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.
Related
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
Get xth Business day of a calendar month. For ex. if Nov'21 then 3rd business day is 3rd November, but if Oct'21 3rd business day is 5th Oct. We need to build a query or function to get this dynamically. We need to exclude the weekends (0,6) and any public holidays (from a table with public holidays)..
I believe we dont have a direct calendar function in postgres, may be we can try getting the input as month and integer for (xth business day) we need to get the output as date..
if input : Nov/11 (Month) and 3 (xth Business Day) it will be output: '2021-11-03' as output
create or replace function nth_bizday(y integer, m integer, bizday integer)
returns date language sql as
$$
select max(d) from
(
select d
from generate_series
(
make_date(y, m, 1),
make_date(y, m, 1) + interval '1 month - 1 day',
interval '1 day'
) t(d)
where extract(isodow from d) < 6
-- and not exists (select from nb_days where nb_day = d)
limit bizday
) t;
$$;
select nth_bizday(2021, 11, 11);
-- 2021-11-15
If you want to skip other non-business days except weekends then the where clause should be extended as #SQLPro suggests, something like this (supposing that you have the non-business days listed in a table, nb_days):
where extract(isodow from d) < 6
and not exists (select from nb_days where nb_day = d)
Business days are generally specific to organization... You must create a CALENDAR table with date and entries from the begining to the end, with a boolean column that indicates if a day is on or off...
Then a view can compute the nth "on" days for every month...
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
Let's say I have a table orders with columns amount and order_date.
I want to be able to group this data by quarters and aggregate the amount, the catch however is that the quarters do not start on January 1st but on any given arbitrary date, say July 12th. These quarters are also split in 13 week intervals. From what I see using something like date_trunc such as:
SELECT SUM(orders.amount), DATE_TRUNC('quarter', orders.order_date) AS interval FROM orders WHERE orders.order_date BETWEEN [date_start] AND [date_end] GROUP BY interval
is out of the question as this forces quarters to start on Jan 1st and it has 'hardcoded' quarter starting dates (Apr 1st, Jul 1st, etc).
I have tried using something like:
SELECT SUM(orders.amount),
to_timestamp(floor((extract('epoch' from orders.order_date / 7862400 )) * 7862400 ) AT TIME ZONE 'UTC' AS interval
FROM orders
WHERE orders.order_date BETWEEN [date_start] AND [date_end]
GROUP BY interval
(where 7862400 is the time interval that I want)
But with this method I cannot figure out how to set the offset for the initial grouping date, in my example I would like it to start from July 12th of each year (then count 13 weeks and start the next quarter, and so on). Hope I was clear and I would appreciate any help!
You can use generate_series() to create the first day of each quarter, join it and group by it.
SELECT quarters.first_day,
quarters.first_day + '13 weeks'::interval last_day,
sum(orders.amount) amount
FROM orders
LEFT JOIN generate_series('2019-07-12'::timestamp,
'2020-07-10'::timestamp,
'13 weeks'::interval) quarters (first_day)
ON quarters.first_day <= orders.order_date
AND quarters.first_day + '13 weeks'::interval > orders.order_date
WHERE orders.order_date BETWEEN [date_start]
AND [date_end]
GROUP BY quarters.first_day,
quarters.first_day + '13 weeks'::interval;
You just need to make sure, that the boundary days you give the generate_series() cover the whole period you want to query, so that depends on your [date_start] and [date_end].
You can generate your own 'quarterly calendar' and use that in place of the Postgers 'quarter' date extraction.
create or replace function quarterly_calendar(annual_date text default extract('YEAR' from current_date)::text)
returns table( quarter integer
, quarter_start_date date
, quarter_end_date date
)
language sql immutable strict leakproof
as $$
with RECURSIVE quarters as
(select 1 qtr, qdt::date q_start_dt, (qdt + interval '90 day' )::date q_end_dt, (qdt+interval '1 year' - interval '1 day')::date last_dt
from ( select date_trunc('year',current_date) + interval '6 month 11 day' qdt) q
union all
select qtr+1, (q_end_dt + interval '1 day')::date, least ((q_end_dt + interval '91 day')::date,last_dt), last_dt
from quarters
where qtr+1 <=5
)
select qtr, q_start_dt, q_end_dt
from quarters;
$$;
-- test
select * from quarterly_calender();
It does actually create 5 quarters. But that is because a year is not a multiple of 13 weeks (or 91 days or 7862400 seconds). In your given year from 12-July-2019 through 11-July-2020 is 2 days (366 days total) over 4 times that interval. You'll have to decide how to handle that 5th quarter. It occurs every year, having either 1 or 2 days. Hope this helps .
How to get the first and last date of the particular month i.e if i pass the particular month name say March it should return output as 01/03/2019 and 31/03/2019.( For current year)
If you want to pass value March you would have to modify the code to understand every month. I'm not sure it's worth the trouble. Anyways, here's a code to return two values (start and end of month) based on current_date. Should you wish to change the day, you could put for example '2019-04-13' in that place.
SELECT
date_trunc('month', current_date) as month_start
, (date_trunc('month', current_date) + interval '1 month' - interval '1 day')::date as month_end
DATE_TRUNC function truncates the date to the precision specified in first argument, thus making the date as of first day of given month (taken from current_date in above example).
For end of month you need a bit more computation. I've always used this in production and what it does is it first truncates your date to first day of month, then adds one month and goes back one day, so that you have your end of month date (whether it's 30, 31, or special case for February during leap years).
for any month, the first day must be 1st,
so it is:
make_date(2019, 3, 1)
and for any month, the last day is 1 day before the first day of next month,
so it is:
make_date(2019, 4, 1) - integer '1'
sorry, I don't have a PostgreSQL environment to test if it is correct,
so please test it yourself.
and, BTW,
you can find more details about date/time operators and functions here:
https://www.postgresql.org/docs/current/functions-datetime.html
One straightforward approach, which would also work on most other databases, would be to truncate the incoming date by month to obtain the first day of that month. Then, truncate the date with one month added to it, and subtract one day, to obtain the last day of the month.
SELECT
DATE_TRUNC('month', '2019-03-15'::date) AS date_start,
DATE_TRUNC('month', '2019-03-15'::date + INTERVAL '1 MONTH')
- INTERVAL '1 DAY' AS date_end;
Demo
From here Date LastDay
SELECT date_trunc('MONTH', dtCol)::DATE;
CREATE OR REPLACE FUNCTION last_day(DATE)
RETURNS DATE AS
$$
SELECT (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::DATE;
$$ LANGUAGE 'sql' IMMUTABLE STRICT;
The conversion from month name parameter is actually rather simple. Create an array with the month names and find the position in the array of the parameter, that result becomes the month value into the make_date function with year extracted from current date and day 1. The below contains an overloaded function providing for either date or month name with optional year.
create type first_last_date as ( first_of date, last_of date);
create or replace function first_last_of_month(date_in date)
returns first_last_date
language sql immutable strict leakproof
as $$
select (date_trunc('month', date_in))::date, (date_trunc('month', date_in) + interval '1 month' - interval '1 day')::date ;
$$;
create or replace function first_last_of_month( month_name_in text
, year_in integer default null
)
returns first_last_date
language sql immutable leakproof
as $$
select first_last_of_month ( make_date ( coalesce (year_in, extract ('year' from now())::integer)
, array_position(ARRAY['jan','feb','mar','apr','may','jun','jul','aug','sep','nov','dec']
, lower(substring(month_name_in,1,3)))
,1 ) );
$$;
-- test
Select first_last_of_month('March');
Select first_last_of_month('February') y2019
, first_last_of_month('February', 2020) y2020;
Select first_last_of_month(now()::date);