Not counting weekend days in an interval - postgresql

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.

Related

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

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

How to truncate a date to the beginning of week (Sunday)?

I need to truncate dates to the start of week, which is Sunday in my case. How can I do this in PostgreSQL? This truncates to Monday:
date_trunc('week', mydate)
If you subtract the dow value (0 for Sundays, 6 for Saturdays) from the current date than you get the previous Sunday which is the begin of your Sunday-based week
demo:db<>fiddle
SELECT
my_date - date_part('dow', my_date)::int
FROM
my_table
Further reading, documentation
You could truncate the date to the week's Monday, then subtract 1 day, e.g:
SELECT (date_trunc('week', now() + interval '1 day') - interval '1 day')::DATE;
date
------------
2019-06-16
As per documentation, date_trunc() accepts values of type date and timestamp and returns a timestamp (thus the cast at the end).

Creating a date series in postgresql 8.3

I am trying to create a series of dates from a fixed date in past to current date, in month increments. I know this is possible in 8.4 with a new feature but i am stuck with 8.3 for now.
I feel I am going down a rabbit hole here as I have this sql to get me monthly increments
SELECT date('2008-01-01') + (to_char(a,'99')||' month')::interval as date FROM generate_series(0,20) as a;
I am then trying to extract months and years from the interval of current date - fixed date
SELECT extract( month from interval (age(current_date, date('2008-01-01'))) );
but im beginning to think this is a silly way to get the desired date series.
Could work like this:
SELECT ('2008-01-01 0:0'::timestamp
+ interval '1 month' * generate_series(0, months))::date
FROM (
SELECT (extract(year from intv) * 12
+ extract(month from intv))::int4 AS months
FROM (SELECT age(now(), '2008-01-01 0:0'::timestamp) as intv) x
) y
In case someone would need e.g. 3 hour interval inside given date range:
SELECT ('2013-01-01 0:0'::timestamp
+ interval '1 hour' * generate_series(0, ('2013-02-01'::date - '2013-01-01'::date)*24, 3))::timestamp;

How to get the number of days in a month?

I am trying to get the following in Postgres:
select day_in_month(2);
Expected output:
28
Is there any built-in way in Postgres to do that?
SELECT
DATE_PART('days',
DATE_TRUNC('month', NOW())
+ '1 MONTH'::INTERVAL
- '1 DAY'::INTERVAL
)
Substitute NOW() with any other date.
Using the smart "trick" to extract the day part from the last date of the month, as demonstrated by Quassnoi. But it can be a bit simpler / faster:
SELECT extract(days FROM date_trunc('month', now()) + interval '1 month - 1 day');
Rationale
extract is standard SQL, so maybe preferable, but it resolves to the same function internally as date_part(). The manual:
The date_part function is modeled on the traditional Ingres equivalent to the SQL-standard function extract:
But we only need to add a single interval. Postgres allows multiple time units at once. The manual:
interval values can be written using the following verbose syntax:
[#] quantity unit[quantity unit...] [direction]
where quantity is a number (possibly signed); unit is microsecond,
millisecond, second, minute, hour, day, week, month, year, decade,
century, millennium, or abbreviations or plurals of these units;
ISO 8601 or standard SQL format are also accepted. Either way, the manual again:
Internally interval values are stored as months, days, and seconds.
This is done because the number of days in a month varies, and a day
can have 23 or 25 hours if a daylight savings time adjustment is
involved. The months and days fields are integers while the seconds
field can store fractions.
(Output / display depends on the setting of IntervalStyle.)
The above example uses default Postgres format: interval '1 month - 1 day'. These are also valid (while less readable):
interval '1 mon - 1 d' -- unambiguous abbreviations of time units are allowed
IS0 8601 format:
interval '0-1 -1 0:0'
Standard SQL format:
interval 'P1M-1D';
All the same.
Note that expected output for day_in_month(2) can be 29 because of leap years. You might want to pass a date instead of an int.
Also, beware of daylight saving : remove the timezone or else some monthes calculations could be wrong (next example in CET / CEST) :
SELECT DATE_TRUNC('month', '2016-03-12'::timestamptz) + '1 MONTH'::INTERVAL
- DATE_TRUNC('month', '2016-03-12'::timestamptz) ;
------------------
30 days 23:00:00
SELECT DATE_TRUNC('month', '2016-03-12'::timestamp) + '1 MONTH'::INTERVAL
- DATE_TRUNC('month', '2016-03-12'::timestamp) ;
----------
31 days
This works as well.
WITH date_ AS (SELECT your_date AS d)
SELECT d + INTERVAL '1 month' - d FROM date_;
Or just:
SELECT your_date + INTERVAL '1 month' - your_date;
These two return interval, not integer.
SELECT cnt_dayofmonth(2016, 2); -- 29
create or replace function cnt_dayofmonth(_year int, _month int)
returns int2 as
$BODY$
-- ZU 2017.09.15, returns the count of days in mounth, inputs are year and month
declare
datetime_start date := ('01.01.'||_year::char(4))::date;
datetime_month date := ('01.'||_month||'.'||_year)::date;
cnt int2;
begin
select extract(day from (select (datetime_month + INTERVAL '1 month -1 day'))) into cnt;
return cnt;
end;
$BODY$
language plpgsql;
You can write a function:
CREATE OR REPLACE FUNCTION get_total_days_in_month(timestamp)
RETURNS decimal
IMMUTABLE
AS $$
select cast(datediff(day, date_trunc('mon', $1), last_day($1) + 1) as decimal)
$$ LANGUAGE sql;