Postgres: first and last days of any month - postgresql

I have two date columns in postgres: 'StartDate' and 'EndDate'.
I need to assign various combinations of date ranges with a different code.
e.g
IF StartDate = EndDate THEN 'a'
What I'd like to be able to do is select any row in which the StartDate is the first day of any month AND the EndDate is the last day of any month. (IF StartDate = FirstDayOfMonth AND EndDate = LastDayOfMonth THEN 'b').
e.g. when StartDate = '01-02-2011' and EndDate = '31-05'2012' then'b', or StartDate = '01-11-1996' and EndDate = '31-01-2001' then 'b'.

The easiest to get the first and last day of a month is to rely on date arithmetics, e.g.:
# select date_trunc('month', now()::date);
date_trunc
------------------------
2014-12-01 00:00:00+01
(1 row)
# select date_trunc('month', now()::date)
+ interval '1 month'
- interval '1 day';
?column?
------------------------
2014-12-31 00:00:00+01
(1 row)
If needed, note that you can use generate_series() to compute the full list between two dates:
select d as first_day,
d + interval '1 month' - interval '1 day' as last_day
from generate_series('2014-01-01'::date,
'2014-12-01'::date,
'1 month') as d;
first_day | last_day
------------------------+------------------------
2014-01-01 00:00:00+01 | 2014-01-31 00:00:00+01
2014-02-01 00:00:00+01 | 2014-02-28 00:00:00+01
2014-03-01 00:00:00+01 | 2014-03-31 00:00:00+02
2014-04-01 00:00:00+02 | 2014-04-30 00:00:00+02
2014-05-01 00:00:00+02 | 2014-05-31 00:00:00+02
2014-06-01 00:00:00+02 | 2014-06-30 00:00:00+02
2014-07-01 00:00:00+02 | 2014-07-31 00:00:00+02
2014-08-01 00:00:00+02 | 2014-08-31 00:00:00+02
2014-09-01 00:00:00+02 | 2014-09-30 00:00:00+02
2014-10-01 00:00:00+02 | 2014-10-31 00:00:00+01
2014-11-01 00:00:00+01 | 2014-11-30 00:00:00+01
2014-12-01 00:00:00+01 | 2014-12-31 00:00:00+01
(12 rows)

You can extract the day part of the date and check its value. For the first day of the month it should be 1, and for the last - you simply add one day to the date and check whether it is the first day of the next month, this would mean that the original date is the last day of the current month.
...
CASE WHEN
EXTRACT('day' FROM StartDate) = 1
AND
EXTRACT('day' FROM EndDate + '1 day'::interval) = 1
THEN 'b'
...

Related

Generate date series by month between two dates and avrage by month in postgresql

I want to create a row for every month between two dates, the first day of every month should be the day of the start date or the first day of every month, and the last date should be the last day of every month or the end date, with average (if date start = 15, then the average should be 15/30) for my table.
input :
product_id | date_start | date_end
1 | 16-01-2020 | 15-03-2020
2 | 07-01-2020 | 22-04-2020
The result should be :
product_id | date_start | date_end | average
1 | 16-01-2020 | 31-01-2020 | 0.5
1 | 01-02-2020 | 29-02-2020 | 1
1 | 01-03-2020 | 15-03-2020 | 0.5
2 | 07-01-2020 | 31-01-2020 | 0.76 -- (30-07)/30
2 | 01-02-2020 | 29-02-2020 | 1
2 | 01-03-2020 | 31-03-2020 | 1
2 | 01-04-2020 | 22-04-2020 | 0.76
I tried using generate series and date trunc and union
SELECT (date_trunc('month', dt) + INTERVAL '1 MONTH' ):: DATE AS date_start ,
(date_trunc('month', dt) + INTERVAL '2 MONTH - 1 day' ):: DATE AS date_end
FROM generate_series( DATE '2020-01-15', DATE '2020-05-21', interval '1 MONTH' ) AS dt
union select '2020-01-15' as date_start,
(date_trunc('month', '2020-01-15'::date) + INTERVAL '1 MONTH - 1 day' ):: DATE AS date_end
union select (date_trunc('month', '2020-05-21'::date) ):: DATE AS date_start ,
'2020-05-21' AS date_end
order by date_start
To adding average I calculate the difference between two dates
SELECT (date_trunc('month', dt) + INTERVAL '1 MONTH' ):: DATE AS date_start ,
(date_trunc('month', dt) + INTERVAL '2 MONTH - 1 day' ):: DATE AS date_end,
((date_trunc('month', dt) + INTERVAL '2 MONTH - 1 day' ) - (date_trunc('month', dt) + INTERVAL '1 MONTH' ):: DATE )
FROM generate_series( DATE '2020-01-15', DATE '2020-05-21', interval '1 MONTH' ) AS dt
with this it seemed like I was hit a wall.
The following gives approximately the same result as you desired, only averages deviates. I believe this stems from an inconsistency in the your calculations where the dates are inclusive in some and excludes either start or end date in others, I was inclusive in all. The other area of difference being I used the actual number of days in the month for denominator calculating it instead of 30. This is necessary for Feb to ever have average 1, otherwise max would be 0.97, and full months having 31 days would average 1.03.
with product_dates(product_id, date_start, date_end) as
( values (1,'2020-01-16'::date,'2020-03-15'::date)
, (2,'2020-01-07'::date,'2020-04-22'::date)
)
select product_id, start_date, end_date, round((end_date-start_date+1 ) * 1.0 / (eom-som+1),2) average
from (select product_id
, greatest(date_start,dt::date) start_date
, least(date_end, (dt+interval '1 month' -interval '1 day')::date) end_date
, dt::date som
, (dt+interval '1 month' -interval '1 day')::date eom
from product_dates
cross join generate_series(date_trunc('month', date_start)
,date_trunc('month', date_end) + interval '1 month' - interval '1 day'
,interval '1 month'
) gs(dt)
) s1;
The heart is the generate_series working directly with dates, notice the date manipulation to ensure I had first day and last day of month. Then in the outer portion of the quest I selected those dates or the parameter date or the generated one (greatest and least functions),

Group by Date and sum of total duration for that day

I am using workbench/j Postgres DB for my query which is as follows -
Input
ID |utc_tune_start_time |utc_tune_end_time
----------------------------------------------
A |04-03-2019 19:00:00 |04-03-2019 20:00:00
----------------------------------------------
A |04-03-2019 23:00:00 |05-03-2019 01:00:00
-----------------------------------------------
A |05-03-2019 10:00:00 |05-03-2019 10:30:00
-----------------------------------------------
Output
ID |Day |Duration in Minutes
----------------------------------------
A |04-03-2019 |120
-----------------------------------
A |05-03-2019 |90
-----------------------------------
I require the duration elapsed from the utc_tune_start_time till the end of the day and similarly, the time elapsed for utc_tune_end_time since the start of the day.
Thanks for your clarifications. This is possible with some case statements. Basically, if utc_tune_start_time and utc_tune_end_time are on the same day, just use the difference, otherwise calculate the difference from the end or start of the day.
WITH all_activity as (
select date_trunc('day', utc_tune_start_time) as day,
case when date_trunc('day', utc_tune_start_time) =
date_trunc('day', utc_tune_end_time)
then utc_tune_end_time - utc_tune_start_time
else date_trunc('day', utc_tune_start_time) +
interval '1 day' - utc_tune_start_time
end as time_spent
from test
UNION ALL
select date_trunc('day', utc_tune_end_time),
case when date_trunc('day', utc_tune_start_time) =
date_trunc('day', utc_tune_end_time)
then null -- we already calculated this earlier
else utc_tune_end_time - date_trunc('day', utc_tune_end_time)
end
FROM test
)
select day, sum(time_spent)
FROM all_activity
GROUP BY day;
day | sum
---------------------+----------
2019-03-04 00:00:00 | 02:00:00
2019-03-05 00:00:00 | 01:30:00
(2 rows)

Postgres expand time window using date_part

Have two dates - '2018-05-01' and '2018-06-01'. I would like to expand this window to the past by day difference of those dates.
SELECT * FROM data
WHERE
start_time > CAST('2018-05-01' AS timestamptz) - INTERVAL '30 DAY'
AND start_time < CAST('2018-06-01' AS timestamptz)
How can I replace INTERVAL '30 DAY' with number of days between given dates without explicitly defining number of days? I know to calculate day difference:
date_part('day',age('2018-05-01', '2018-06-01'))
But not sure how to incorporate into the substraction. Dates and days between them will change.
You can use date_trunc('mon', some_date_expression) to round down to the start of a month:
select date_trunc('mon', now() - '3 mon'::interval) as date_begin
, date_trunc('mon', now() - '1 day'::interval) as date_end
;
Result
date_begin | date_end
------------------------+------------------------
2018-03-01 00:00:00+01 | 2018-06-01 00:00:00+02
(1 row)
You can simply subtract the difference from the start date:
with t (start_date, end_date) as (
values (date '2018-05-01', date '2018-06-01')
)
select start_date - (end_date - start_date) as new_start,
end_date
from t;
returns
new_start | new_end
-----------+-----------
2018-03-31 | 2018-06-01

PostgreSQL custom week number - first week containing Feb 1st

I'm new to SQL functions and trying to create a calendar table that displays custom week numbers, with each week starting with Saturday and ending on Friday. The first week of each year always contains Feb.1st of that year.
For example, if the day of the week of Feb. 1st for a particular year is Tuesday, then the first week for that year is from Jan. 29 to Feb. 4.
I've been struggling with this problem for a couple days and the only solution I can come up with is as follows:
First, I created a calendar table with a column called "CustomizedWeekNo" to reflect the year cycle starting from the week containing Feb. 1st. But the first day of each week is Monday.
Create Table Calendar
(CalendarDate Date, WeekNo smallInt, WeekDayNo text, CustomizedWeekNo smallInt)
Create or Replace Function CustomizeWeekNumber()
Returns void
as $$
Declare beginDate Date :='2015-01-31'; endDate Date := '2017-01-27';
Begin
While beginDate <= endDate loop
Insert Into Calendar (CalendarDate, WeekNo, WeekDayNo, CustomizedWeekNo)
Select
beginDate As CalendarDate
,DATE_PART('week', beginDate::timestamp)::smallint As WeekNo
,(Case When DATE_PART('isodow', beginDate::timestamp)::smallint = 6
Then 'Sat'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 7
Then 'Sun'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 1
Then 'Mon'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 2
Then 'Tue'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 3
Then 'Wed'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 4
Then 'Thur'
Else 'Fri'
End) As WeekDayNo;
,(Case When beginDate < '2016-01-04'
Then DATE_PART('week', beginDate::timestamp)::smallint - 5
When beginDate >= '2016-01-04' and beginDate < '2016-01-30'
Then (date_part('week', '2016-01-03'::timestamp)::smallint - 5 + date_part('week', beginDate::timestamp)::smallint)
When beginDate >= '2016-01-30' and beginDate < '2017-01-02'
Then date_part('week', beginDate::timestamp)::smallint - 4
Else
date_part('week', '2017-01-01'::timestamp)::smallint - 4 + date_part('week', beginDate::timestamp)::smallint
End) As CustomizedWeekNo;
Select (beginDate + interval'1 day') into beginDate;
End loop;
End; $$
language plpgsql;
# Run the function
select CustomizeWeekNumber()
Next, I update the "CustomizedWeekNo" column
-- My customized week starts from every Saturday and ends on every Friday
update calendar
set CustomizedWeekNo = CustomizedWeekNo + 1
where WeekDayNo in ('Sat', 'Sun');
Lastly, I create another function to return the information I need. I also reformat the value of the "CustomizedWeekNo" to include the specific year.
create or replace function update_CustomizedWeek(date, date)
returns table(Calendar_Date Date, Week_No int, WeekDay_No text, Customized_Week_No int)
as $$
begin
return query
select t.CalendarDate, t.WeekNo, t.WeekDayNo,
case when t.CustomizedWeekNo <= 9
then (date_part('year', t.CalendarDate::timestamp)::text||'0'||t.CustomizedWeekNo::text)::int
else (date_part('year', t.CalendarDate::timestamp)::text||t.CustomizedWeekNo::text)::int
end
from Calendar t
where t.CalendarDate >= $1 and t.CalendarDate <= $2
order by t.CalendarDate;
end; $$
language plpgsql;
--Example
select * from update_CustomizedWeek('2015-01-30', '2015-02-10')
The final result will look like:
Calendar_Date | Week_No | WeekDay_No | Customized_Week_No
------------- | ------- | ---------- | -------------------
2015-01-31 | 5 | Sat | 201501
2015-02-01 | 5 | Sun | 201501
2015-02-02 | 6 | Mon | 201501
2015-02-03 | 6 | Tue | 201501
2015-02-04 | 6 | Wed | 201501
2015-02-05 | 6 | Thur | 201501
2015-02-06 | 6 | Fri | 201501
2015-02-07 | 6 | Sat | 201502
2015-02-08 | 6 | Sun | 201502
2015-02-09 | 7 | Mon | 201502
2015-02-10 | 7 | Tue | 201502
As you can see, I used a lot of "hard coding" here. I would like to be able to generate a date range along with the customized week number for any year, not just 2016 or 2017. Any help is really appreciated.
date_trunc() truncates to the previous Monday. You can still use it by adding 2 days to the input (the difference between Sat and Mon), then subtract 2 days from the output. Works perfectly.
This query produces your desired output exactly:
SELECT d::date AS "Calendar_Date"
, EXTRACT('WEEK' FROM d)::int AS "Week_No"
, to_char(d, 'Dy') AS "WeekDay_No"
, base_nr + (rn::int - 1) / 7 AS "Customized_Week_No"
FROM (
SELECT date_trunc('week', feb3) - interval '2 days' AS day1 -- subtract 2 days
, EXTRACT('year' FROM feb3)::int * 100 + 1 AS base_nr
FROM (SELECT timestamp '2015-02-03') input(feb3) -- add 2 days, so Feb 3 (!)
) t, generate_series (day1
, day1 + interval '1 year - 1 day'
, interval '1 day') WITH ORDINALITY AS d(d, rn);
Just provide Feb 3 of the respective year: timestamp '2015-02-03'.

pgSQL date function

How do I tweak this pgSQL SELECT using pgSQL's date functions so it always returns "01" for the day # and "00:00:00" for the time?
SELECT s.last_mailing + '1 month'::interval AS next_edition_date FROM
last_mailing is defined as
last_mailing timestamp without time zone
Examples of the result I am wanting are:
2015-10-01 00:00:00
2015-11-01 00:00:00
2015-12-01 00:00:00
You're looking for date_trunc().
psql (9.5alpha2, server 9.4.4)
Type "help" for help.
testdb=# create table subscriptions as select 1 "id", '2015-07-14T12:32'::timestamp last_mailing union all select 2, '2015-08-15T00:00';
SELECT 2
testdb=# select * from subscriptions; id | last_mailing
----+---------------------
1 | 2015-07-14 12:32:00
2 | 2015-08-15 00:00:00
(2 rows)
testdb=# select *, date_trunc('month', last_mailing) + interval '1 month' AS next_edition_date from subscriptions;
id | last_mailing | next_edition_date
----+---------------------+---------------------
1 | 2015-07-14 12:32:00 | 2015-08-01 00:00:00
2 | 2015-08-15 00:00:00 | 2015-09-01 00:00:00
(2 rows)
If you want the first day of the next month, then use:
SELECT (s.last_mailing - (1 - extract(day from s.last_mailing)) * interval '1 day') +
interval '1 month' AS next_edition_date
FROM . . .
If you don't want the time, then use date_trunc():
SELECT date_trunc('day',
(s.last_mailing - (1 - extract(day from s.last_mailing)) * interval '1 day') +
interval '1 month'
) AS next_edition_date
FROM . . .