How to select a certain future date based on integer and integer[] in Postgresql? - postgresql

I am trying to create a query that will return information about a series of future dates. So for example, today is Monday, and I want to get three days worth of information in advance: Tuesday, Wednesday, and Thursday. I understand how to use something like generate_series with a starting and end date to get the rows.
The problem I'm having is, I am selecting an integer for the number of days in advance I want from one table from a second table. But the particular dates will change if one or more of the potential future dates is one where the business is not open. So if the starting date were Thursday, and the business is closed on Sunday, I'd want to get rows for Friday, Saturday, and Monday.
So from the first table with the specifics on which days to get, I'd be selecting an integer (e.g. 3) and an integer[] (e.g. {1,2,3,4,5,6}). My thought was to somehow start with the day of the week of tomorrow (e.g. 2 from SELECT EXTRACT(DOW FROM CURRENT_DATE + '1 days'::interval)if today is tomorrow is Tuesday) and then check if that DOW is inside the array. I'd have a separate counter with the number of extra days I'd need to add to my series, and after looping through until I get three days that aren't skipped, I'd add it to my days ahead number. So starting on Thursday, I'd check Friday (5), it's in the array, increment loop variable and continue. Saturday (6), it's in the array, increment loop variable and continue. Sunday (0), not in the array, add one to the extra days counter and continue. Monday (1), in the array, increment loop variable and continue. That's three, so I'm done. Then add my second counter (1) to the original days ahead (3) and get 4 days worth of information. Days that the business isn't open will be excluded through WHERE conditions, so the total number of days displayed will be consistent.
The problem is, I can conceptualize this solution, but I can't figure out how to put it together syntactically. Here's an approximation of what I think would work:
DO $$
BEGIN
DECLARE
counter integer := 0;
increment_days integer := 1;
WITH future_data AS
(SELECT days_ahead, open_days FROM Stores);
WHILE counter < (SELECT days_ahead FROM future_data) loop
CASE WHEN (SELECT EXTRACT(DOW FROM CURRENT_DATE + (days::text || ' days'::interval))
= ANY(SELECT unnest(open_days) FROM future_data)) THEN
counter := counter + 1;
ELSE counter := counter END;
increment_days := increment_days + 1;
END LOOP;
increment_days := increment_days + days_ahead;
--[...main SELECT query...]
END$$;
I keep getting complains about the way I'm putting this all together. Currently it's a syntax error at WHILE. It seems like I can't do anything but a SELECT statement there.

Rather the trying to figure out how many days in advance just build a function where you provide a start_date and the number of days you want. Then let the function determine the actual dates returned (ie it bypasses Sunday). The following SQL function does that using a recursive CTE rather than attempting to calculate the number of days to look forward. See fiddle
create or replace
function business_day(start_date_in date, num_days_in integer default 3)
returns setof date
language sql
immutable strict
as $$
with recursive get_days (bus_date, num_selected) as
( select case when extract(dow from start_date_in::timestamp) > 0
then start_date_in::timestamp + interval '1 day'
else start_date_in::timestamp + interval '2 day'
end
, 1
union all
select case when extract(dow from bus_date + interval '1 day')>0
then bus_date + interval '1 day'
else bus_date + interval '2 day'
end
, num_selected + 1
from get_days
where num_selected<num_days_in
)
select bus_date::date from get_days ;
$$;

Related

PostgreSQL to get the xth business day for the given month

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...

Not counting weekend days in an interval

I want to get a timestamp which is X week days before another timestamp.
I can only get to a timestamp which is X days before another timestamp:
select item.timestamp - (X * interval '1' day)
How can I upgrade this query to not count weekend days?
You would want to substract 7 days for every 5 days, plus subtract the remainder days (after dividing by 5), and subtract another two days if the remainder subtraction will put you back into/through a weekend. So something like this:
create or replace function minus_weekdays(timestamptz, int) returns timestamptz language sql as $$
select $1 - interval '1 day' * case
when extract(DOW from $1+interval '1 day') <= ($2%5)+1
then 7*($2/5)+($2%5)+2
else 7*($2/5)+($2%5) end;
$$;
This will probably do the wrong thing if called on a date which is already a weekend day, but I don't know what you want to do that in that case.

'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.

How to write the query to get the first and last date of a January and other month's in postgresql

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);

PostgreSQL Selecting The Closest Previous Month of June

I am trying to write a piece for a query that grabs the closest, past June 1st. For example, today is 10/2/2018. If I run the query today, I need it to use the date 6/1/2018. If I run it on 5/29/2019, it still needs to grab 6/1/2018. If I run it on 6/2/2019, it should then grab 6/1/2019. If I run it on 6/2/2022, it should then grab 6/1/2022 and so on.
I believe I need to start with something like this:
SELECT CASE WHEN EXTRACT(MONTH FROM NOW())>=6 THEN 'CURRENT' ELSE 'RF LAST' END AS X
--If month is greater than or equal to 6, you are in the CURRENT YEAR (7/1/CURRENT YEAR)
--If month is less than 6, then reference back to the last year (YEAR MINUS ONE)
And I believe I need to truncate the date then perform an operation. I am unsure of which approach to take (if I should be adding a year to a timestamp such as '6/1/1900', or if I should try to disassemble the date parts to perform an operation. I keep getting errors in my attempts such as "operator does not exist". Things I have tried include:
SELECT (CURRENT_DATE- (CURRENT_DATE-INTERVAL '7 months'))
--This does not work as it just gives me a count of days.
SELECT (DATE_TRUNC('month',NOW())+TIMESTAMP'1900-01-01 00:00:00')
--Variations of this just don't work and generally error out.
Use a case expression to determine if you need to use the current year, or, the previous year (months 1 to 5)
case when extract(month from current_date) >= 6 then 0 else -1 end
then add that to the year extracted from current_date, e.g. using to_date()
select to_Date('06' || (extract(year from current_date)::int + case when extract(month from current_date) >= 6 then 0 else -1 end)::varchar, 'mmYYYY');
You could also use make_date(year int, month int, day int) in postgres 9.4+
select make_date(extract(year from current_date) + case when extract(month from current_date) >= 6 then 0 else -1 end, 6, 1) ;
If month lower than 6, trunc year and minus 6 months.
Else trunc year and add 6 months.
set datestyle to SQL,MDY;
select
case when (extract( month from (date::date)))<6 then date_trunc('year',date)-'6 month'::interval
else date_trunc('year',date)+'6 months'::interval
end as closest_prev_june,
another_column,
another_column2
from mytable;
But format is default and supposed you have a column that named date.
If you want to do this with now(), change date columns with now()
function.