How do you get a dynamic 12 business day view in Postgresql? - postgresql

Here is the code I currently have that gives me the last 12 days
SELECT *
FROM table
WHERE analysis_date >= current_date - interval '12' day;
analysis_date is the date column in the table. I understand why this isn't working because it's not accounting for business days. How can I rewrite this so that I get an interval of the last 12 business days?
I tried search online and found
extract(dow from (date))
But I couldn't find an example where I need a weekday interval. Any help would be appreciated.

This can be solved with a CTE:
WITH business_days_back AS (
WITH RECURSIVE bd(back_day, go_back) AS (
-- Go back to the previous Monday, allowing for current_date in the weekend
SELECT CASE extract(dow from current_date)
WHEN 0 THEN current_date - 6
WHEN 6 THEN current_date - 5
ELSE current_date - extract(dow from current_date)::int + 1
END,
CASE extract(dow from current_date)
WHEN 0 THEN 7
WHEN 6 THEN 7
ELSE 12 - extract(dow from current_date)::int + 1
END
UNION
-- Go back by the week until go_back = 0
SELECT CASE
WHEN go_back >= 5 THEN back_day - 7
WHEN go_back > 0 THEN back_day - 2 - go_back
END,
CASE
WHEN go_back >= 5 THEN go_back - 5
WHEN go_back > 0 THEN 0
END
FROM bd
)
SELECT back_day FROM bd WHERE go_back = 0
)
SELECT * FROM my_table WHERE analysis_date >= (SELECT * FROM business_days_back);
Some explanation:
The inner CTE starts off by working back to the previous Monday, compensating for a current_date that falls on a weekend day.
The recursive term then adds rows by going back full weeks (back_day - 7 for the calendar date and go_back - 5 for the business days) until go_back = 0.
The outer CTE returns the back_day date where go_back = 0. This is therefore a scalar query and you can use it as a sub-query in a filter expression.
You can change the number of business days to look back by simply changing the numbers 12 and 7 in the initial SELECT in the inner CTE. Keep in mind, though, that the value should be such that it goes back to the previous Monday or the query will fail, due to the same initial SELECT of the inner CTE.
A far more flexible (and probably faster*) solution is to use the following function:
CREATE FUNCTION business_days_diff(from_date date, diff int) RETURNS date AS $$
-- This function assumes Mon-Fri business days
DECLARE
start_dow int;
calc_date date;
curr_diff int;
weekend int;
BEGIN
-- If no diff requested, return the from_date. This may be a non-business day.
IF diff = 0 THEN
RETURN from_date;
END IF;
start_dow := extract(dow from from_date)::int;
calc_date := from_date;
IF diff < 0 THEN -- working backwards
weekend := -2;
IF start_dow = 0 THEN -- Fudge initial Sunday to the previous Saturday
calc_date := calc_date - 1;
start_dow := 6;
END IF;
IF start_dow + diff >= 1 THEN -- Stay in this week
RETURN calc_date + diff;
ELSE -- Work back to Monday
calc_date := calc_date - start_dow + 1;
curr_diff := diff + start_dow - 1;
END IF;
ELSE -- Working forwards
weekend := 2;
IF start_dow = 6 THEN -- Fudge initial Saturday to the following Sunday
calc_date := calc_date + 1;
start_dow := 0;
END IF;
IF start_dow + diff <= 5 THEN -- Stay in this week
RETURN calc_date + diff;
ELSE -- Work forwards to Friday
calc_date := calc_date + 5 - start_dow;
curr_diff := diff - 5 + start_dow;
END IF;
END IF;
-- Move backwards or forwards by full weeks
calc_date := calc_date + (curr_diff / 5) * 7;
-- Process any remaining days, include weekend
IF curr_diff % 5 != 0 THEN
RETURN calc_date + curr_diff % 5 + weekend;
ELSE
RETURN calc_date;
END IF;
END; $$ LANGUAGE plpgsql STRICT IMMUTABLE;
This function can take any date to calculate from and any number of days into the future (positive value of diff) or the past (negative value of diff), including diffs within the current week. And since it returns the business day date as a scalar, use in your query is very straightforward:
SELECT *
FROM table
WHERE analysis_date >= business_days_diff(current_date, -12);
Apart from that, you can also pass in fields from your table and do funky stuff like:
SELECT t1.some_value - t2.some_value AS value_diff
FROM table t1
JOIN table t2 ON t2.analysis_date = business_days_diff(t1.analysis_date, -12);
i.e. a self-join on a certain number of business days separation.
Note that this function assumes a Monday-Friday business day week.
* This function does only simple arithmetic on scalar values. The CTE has to set up all manner of structures to support the iteration and the resulting record sets.

Related

how to recognize if month has 30 or 31 days in sqlg

I need to get last of the five days from month like:
1-5 = 5
6-10 = 10
...
26-30/31 = 30/31 (here can be 6 days depending on the month)
I've prepared function like
create or replace function getfirstdayoffive()
returns date
as
$$
select date_trunc('month', current_date - 5)::date
+ (least(ceil(extract(day from current_date - 5) / 5) * 5,
date_part('day', date_trunc('month', startOp) + interval '1 month - 1 day')))::int - 1;
$$
language sql
stable;
and it is working fine to return last day of five. How can I modify it so it would recognize if the last period should have 5 or 6 days?
try following function:
CREATE OR REPLACE FUNCTION public.days_in_month(d date)
RETURNS integer
LANGUAGE sql
AS $function$
SELECT date_trunc('month', $1::timestamp + interval '1 month')::date
- date_trunc('month', $1::timestamp)::date;
$function$
Determine the last date of the month, then extract day. If the day is 31 then return minus 6 days, else return minus five days. That assumes you want the last 5 days for Feb. But then except for Feb you could just return the 25th of the month as that is what minus 5 for months with 30 days and minus 6 for days 31 always returns. Note: rather than hard coding current_date this allows a parameter with default value of current_date.
create or replace
function getfirstdayoffive(parm_date_in date default current_date)
returns date
language sql
immutable strict
as $$
with last_of_mon(eom) as
( select date_trunc('month', parm_date_in) + interval '1 month - 1 day' )
select case when extract(day from eom) = 31
then (eom-interval '6 days')::date
else (eom-interval '5 days')::date
end
from last_of_mon;
$$;
select * from getfirstdayoffive();
select * from getfirstdayoffive(date '2021-08-15');
select * from getfirstdayoffive(date '2020-02-15');
hmm for now I've got something like :
create or replace
function getlastdayoffive(parm_date_in date default current_date)
returns timestamp
language sql
immutable strict
as $$
with last_of_mon(eom) as
( select date_trunc('month', parm_date_in) + interval '1 month - 1 day' )
select case when extract(day from eom) = 31
then (least(ceil(extract(day from parm_date_in - 5) / 5) * 5, date_part('day', date_trunc('month', parm_date_in) + interval '1 month - 1 day')))::timestamp
else (least(ceil(extract(day from parm_date_in - 6) / 6) * 6, date_part('day', date_trunc('month', parm_date_in) + interval '1 month - 1 day')))::timestamp
end
from last_of_mon;
$$;
But I can't cast to timestamp, how can it be done? if I return integer then I got what I wanted, but the poblem is I need to have a full date in format YYYY-MM-dd

How to select a certain future date based on integer and integer[] in 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 ;
$$;

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

Count months between two timestamp on postgresql?

I want to count the number of months between two dates.
Doing :
SELECT TIMESTAMP '2012-06-13 10:38:40' - TIMESTAMP '2011-04-30 14:38:40';
Returns :
0 years 0 mons 409 days 20 hours 0 mins 0.00 secs
and so:
SELECT extract(month from TIMESTAMP '2012-06-13 10:38:40' - TIMESTAMP '2011-04-30 14:38:40');
returns 0.
age function returns interval:
age(timestamp1, timestamp2)
Then we try to extract year and month out of the interval and add them accordingly:
select extract(year from age(timestamp1, timestamp2)) * 12 +
extract(month from age(timestamp1, timestamp2))
Please note that the most voted answer by #ram and #angelin is not accurate when you are trying to get calendar month difference using.
select extract(year from age(timestamp1, timestamp2))*12 + extract(month from age(timestamp1, timestamp2))
for example, if you try to do:
select extract(year from age('2018-02-02'::date, '2018-03-01'::date))*12 + extract(month from age('2018-02-02'::date , '2018-03-01'::date))
the result will be 0 but in terms of months between March from February should be 1 no matter the days between dates.
so the formula should be like the following saying that we start with timestamp1 and timestamp2:
((year2 - year1)*12) - month1 + month2 = calendar months between two timestamps
in pg that would be translated to:
select ((extract('years' from '2018-03-01 00:00:00'::timestamp)::int - extract('years' from '2018-02-02 00:00:00'::timestamp)::int) * 12)
- extract('month' from '2018-02-02 00:00:00'::timestamp)::int + extract('month' from '2018-03-01 00:00:00'::timestamp)::int;
you can create a function like:
CREATE FUNCTION months_between (t_start timestamp, t_end timestamp)
RETURNS integer
AS $$
select ((extract('years' from $2)::int - extract('years' from $1)::int) * 12)
- extract('month' from $1)::int + extract('month' from $2)::int
$$
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
The age function give a justified interval to work with:
SELECT age(TIMESTAMP '2012-06-13 10:38:40', TIMESTAMP '2011-04-30 14:38:40');
returns 1 year 1 mon 12 days 20:00:00, and with that you can easily use EXTRACT to count the number of months:
SELECT EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age) AS months_between
FROM age(TIMESTAMP '2012-06-13 10:38:40', TIMESTAMP '2011-04-30 14:38:40') AS t(age);
If you will do this multiple times, you could define the following function:
CREATE FUNCTION months_between (t_start timestamp, t_end timestamp)
RETURNS integer
AS $$
SELECT
(
12 * extract('years' from a.i) + extract('months' from a.i)
)::integer
from (
values (justify_interval($2 - $1))
) as a (i)
$$
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
so that you can then just
SELECT months_between('2015-01-01', now());
SELECT date_part ('year', f) * 12
+ date_part ('month', f)
FROM age ('2015-06-12', '2014-12-01') f
Result: 6 Months
Gives the differenece of months of two dates
SELECT ((extract( year FROM TIMESTAMP '2012-06-13 10:38:40' ) - extract( year FROM TIMESTAMP '2011-04-30 14:38:40' )) *12) + extract(MONTH FROM TIMESTAMP '2012-06-13 10:38:40' ) - extract(MONTH FROM TIMESTAMP '2011-04-30 14:38:40' );
The Result : 14
Have to extract months seperately for both the dates and then the difference of both the results
Here is a PostgreSQL function with the exact same behavior as the Oracle MONTHS_BETWEEN function.
It has been tested on a wide range of years (including leap ones) and more than 700k combinations of dates (including end of every months).
CREATE OR REPLACE FUNCTION months_between
( DATE,
DATE
)
RETURNS float
AS
$$
SELECT
(EXTRACT(YEAR FROM $1) - EXTRACT(YEAR FROM $2)) * 12
+ EXTRACT(MONTH FROM $1) - EXTRACT(MONTH FROM $2)
+ CASE
WHEN EXTRACT(DAY FROM $2) = EXTRACT(DAY FROM LAST_DAY($2))
AND EXTRACT(DAY FROM $1) = EXTRACT(DAY FROM LAST_DAY($1))
THEN
0
ELSE
(EXTRACT(DAY FROM $1) - EXTRACT(DAY FROM $2)) / 31
END
;
$$
LANGUAGE SQL
IMMUTABLE STRICT;
This function requires a LAST_DAY function (behaving the same as Oracle's one) :
CREATE OR REPLACE FUNCTION last_day
( DATE
)
RETURNS DATE
AS
$$
SELECT
(DATE_TRUNC('MONTH', $1) + INTERVAL '1 MONTH' - INTERVAL '1 DAY')::date
;
$$
LANGUAGE SQL
IMMUTABLE STRICT;
I had the same problem once upon a time and wrote this ... it's quite ugly:
postgres=> SELECT floor((extract(EPOCH FROM TIMESTAMP '2012-06-13 10:38:40' ) - extract(EPOCH FROM TIMESTAMP '2005-04-30 14:38:40' ))/30.43/24/3600);
floor
-------
85
(1 row)
In this solution "one month" is defined to be 30.43 days long, so it may give some unexpected results over shorter timespans.
Extract by year and months will floor on months:
select extract(year from age('2016-11-30'::timestamp, '2015-10-15'::timestamp)); --> 1
select extract(month from age('2016-11-30'::timestamp, '2015-10-15'::timestamp)); --> 1
--> Total 13 months
This approach maintains fractions of months (thanks to tobixen for the divisor)
select round(('2016-11-30'::date - '2015-10-15'::date)::numeric /30.43, 1); --> 13.5 months
Try this solution:
SELECT extract (MONTH FROM age('2014-03-03 00:00:00'::timestamp,
'2013-02-03 00:00:00'::timestamp)) + 12 * extract (YEAR FROM age('2014-03-03
00:00:00'::timestamp, '2013-02-03 00:00:00'::timestamp)) as age_in_month;
SELECT floor(extract(days from TIMESTAMP '2012-06-13 10:38:40' - TIMESTAMP
'2011-04-30 14:38:40')/30.43)::integer as months;
Gives an approximate value but avoids duplication of timestamps. This uses hint from tobixen's answer to divide by 30.43 in place of 30 to be less incorrect for long timespans while computing months.
I made a function like this:
/* similar to ORACLE's MONTHS_BETWEEN */
CREATE OR REPLACE FUNCTION ORACLE_MONTHS_BETWEEN(date_from DATE, date_to DATE)
RETURNS REAL LANGUAGE plpgsql
AS
$$
DECLARE age INTERVAL;
declare rtn real;
BEGIN
age := age(date_from, date_to);
rtn := date_part('year', age) * 12 + date_part('month', age) + date_part('day', age)/31::real;
return rtn;
END;
$$;
Oracle Example)
SELECT MONTHS_BETWEEN
(TO_DATE('2015-02-02','YYYY-MM-DD'), TO_DATE('2014-12-01','YYYY-MM-DD') )
"Months" FROM DUAL;
--result is: 2.03225806451612903225806451612903225806
My PostgreSQL function example)
select ORACLE_MONTHS_BETWEEN('2015-02-02'::date, '2014-12-01'::date) Months;
-- result is: 2.032258
From the result you can use CEIL()/FLOOR() for rounding.
select ceil(2.032258) --3
select floor(2.032258) --2
Try;
select extract(month from age('2012-06-13 10:38:40'::timestamp, '2011-04-30 14:38:40'::timestamp)) as my_months;

Finding previous day of the week

In PostgreSQL 8.4, given a date, if that date is not a Friday, I would like to find the date of the previous Friday. Can someone tell me if there is an inbuilt function or give the logic behind getting my own function.
Try this, works on other days too, blog about it http://www.ienablemuch.com/2010/12/finding-previous-day-of-week.html
create or replace function previous_date_of_day(the_date date, dow int) returns date
as
$$
select
case when extract(dow from $1) < $2 then
$1 - ( extract(dow from $1) + (7 - $2) )::int
else
$1 - ( extract(dow from $1) - $2)::int
end;
$$ language 'sql';
select to_char(z.ds, 'Mon dd yyyy dy') as source,
to_char( previous_date_of_day(z.ds, 5), 'Mon dd yyyy dy') as dest
from
(
select 'Dec 1 2010'::date + x.n as ds
from generate_series(0,17) as x(n)
) as z
You solve it without using case:
select
the_date
from
(
select
now()::date - num as the_date, -- generate rows of possible dates
extract(dow from (now()::date - num)) -- dow for the where condition
from (select generate_series(0,6) as num) as t
) as days
where date_part = 5;
SELECT
CASE
-- 1. if Friday, return date
WHEN EXTRACT(DOW FROM my_date) = 5
THEN my_date
-- 2. if Saturday, subtract 1
WHEN EXTRACT(DOW FROM my_date) = 6
THEN my_date - INTERVAL '1 day'
-- 3. all other days of the week, subtract `DOW + 2` from my_date
-- should be ELSE for future-proofing ;-) MB
ELSE -- WHEN EXTRACT(DOW FROM my_date) < 5 THEN
my_date - ((EXTRACT(DOW FROM my_date) + 2)::TEXT||'days')::INTERVAL
END AS tgif
FROM
my_table
WHERE
my_date IS NOT NULL
select case when extract(dow from your_date) < 5 then
your_date - (extract(dow from your_date) + integer '2')
else when extract(dow from your_date) > 5 then
your_date - integer '1'
else
your_date
end
Reference http://developer.postgresql.org/pgdocs/postgres/functions-datetime.html