I work with a Postgres database. This DB has a table with users, who have a birthdate (date field). Now I want to get all users who have their birthday in the upcoming week....
My first attempt: SELECT id FROM public.users WHERE id IN (lange reeks) AND birthdate > NOW() AND birthdate < NOW() + interval '1 week'
But this does not result, obviously because off the year. How can I work around this problem?
And does anyone know what happen to PG would go with the cases at 29-02 birthday?
We can use a postgres function to do this in a really nice way.
Assuming we have a table people, with a date of birth in the column dob, which is a date, we can create a function that will allow us to index this column ignoring the year. (Thanks to Zoltán Böszörményi):
CREATE OR REPLACE FUNCTION indexable_month_day(date) RETURNS TEXT as $BODY$
SELECT to_char($1, 'MM-DD');
$BODY$ language 'sql' IMMUTABLE STRICT;
CREATE INDEX person_birthday_idx ON people (indexable_month_day(dob));
Now, we need to query against the table, and the index. For instance, to get everyone who has a birthday in April of any year:
SELECT * FROM people
WHERE
indexable_month_day(dob) >= '04-01'
AND
indexable_month_day(dob) < '05-01';
There is one gotcha: if our start/finish period crosses over a year boundary, we need to change the query:
SELECT * FROM people
WHERE
indexable_month_day(dob) >= '12-29'
OR
indexable_month_day(dob) < '01-04';
To make sure we match leap-day birthdays, we need to know if we will 'move' them a day forward or backwards. In my case, it was simpler to just match on both days, so my general query looks like:
SELECT * FROM people
WHERE
indexable_month_day(dob) > '%(start)%'
%(AND|OR)%
indexable_month_day(dob) < '%(finish)%';
I have a django queryset method that makes this all much simpler:
def birthday_between(self, start, finish):
"""Return the members of this queryset whose birthdays
lie on or between start and finish."""
start = start - datetime.timedelta(1)
finish = finish + datetime.timedelta(1)
return self.extra(where=["indexable_month_day(dob) < '%(finish)s' %(andor)s indexable_month_day(dob) > %(start)s" % {
'start': start.strftime('%m-%d'),
'finish': finish.strftime('%m-%d'),
'andor': 'and if start.year == finish.year else 'or'
}]
def birthday_on(self, date):
return self.birthday_between(date, date)
Now, I can do things like:
Person.objects.birthday_on(datetime.date.today())
Matching leap-day birthdays only on the day before, or only the day after is also possible: you just need to change the SQL test to a `>=' or '<=', and not adjust the start/finish in the python function.
I'm not overly confident in this, but it seems to work in my testing. The key here is the OVERLAPS operator, and some date arithmetic.
I assume you have a table:
create temporary table birthdays (name varchar, bday date);
Then I put some stuff into it:
insert into birthdays (name, bday) values
('Aug 24', '1981-08-24'), ('Aug 04', '1982-08-04'), ('Oct 10', '1980-10-10');
This query will give me the people with birthdays in the next week:
select * from
(select *, bday + date_trunc('year', age(bday)) + interval '1 year' as anniversary from birthdays) bd
where
(current_date, current_date + interval '1 week') overlaps (anniversary, anniversary)
The date_trunc truncates the date at the year, so it should get you up to the current year. I wound up having to add one year. This suggests to me I have an off-by-one in there for some reason. Perhaps I just need to find a way to get dates to round up. In any case, there are other ways to do this calculation. age gives you the interval from the date or timestamp to today. I'm trying to add the years between the birthday and today to get a date in the current year.
The real key is using overlaps to find records whose dates overlap. I use the anniversary date twice to get a point-in-time.
Finally, to show the upcoming birthdays of the next 14 days I used this:
SELECT
-- 14 days before birthday of 2000
to_char( to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') - interval '14 days' , 'YYYY-MM-dd') as _14b_b2000,
-- birthday of 2000
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') as date_b2000,
-- current date of 2000
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') as date_c2000,
-- 14 days after current date of 2000
to_char( to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') + interval '14 days' , 'YYYY-MM-dd') as _14a_c2000,
-- 1 year after birthday of 2000
to_char( to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') + interval '1 year' , 'YYYY-MM-dd') as _1ya_b2000
FROM c
WHERE
-- the condition
-- current date of 2000 between 14 days before birthday of 2000 and birthday of 2000
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') between
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') - interval '14 days' and
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd')
or
-- 1 year after birthday of 2000 between current date of 2000 and 14 days after current date of 2000
to_date(to_char(c.birthdate, '2000-MM-dd'), 'YYYY-MM-dd') + interval '1 year' between
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') and
to_date(to_char(current_date, '2000-MM-dd'), 'YYYY-MM-dd') + interval '14 days'
;
So:
To solve the leap-year issue, I set both birthdate and current date to 2000,
and handle intervals only from this initial correct dates.
To take care of the near end/beginning dates,
I compared first the 2000 current date to the 2000 birthday interval,
and in case current date is at the end of the year, and the birthday is at the beginning,
I compared the 2001 birthday to the 2000 current date interval.
Here's a query that gets the right result, most of the time.
SELECT
(EXTRACT(MONTH FROM DATE '1980-08-05'),
EXTRACT(DAY FROM DATE '1980-08-05'))
IN (
SELECT EXTRACT(MONTH FROM CURRENT_DATE + s.a) AS m,
EXTRACT(DAY FROM CURRENT_DATE + s.a) AS d
FROM GENERATE_SERIES(0, 6) AS s(a)
);
(it doesn't take care of leap years correctly; but you could use extract again to work the subselect in terms of a leap year instead of the current year.
EDIT: Got it working for all cases, and as a useful query rather than a scalar select. I'm using some extra subselects so that I don't have to type the same date or expression twice for month and day, and of course the actual data would be in a table instead of the values expression. You might adapt this differently. It might still stand to improve by making a more intelligent series for weeks containing leap days, since sometimes that interval will only contain 6 days (for non-leap years).
I'll try to explain this from the inside-out; First thing I do is normalize the target date (CURRENT_DATE usually, but explicit in this code) into a year that I know is a leap year, so that February 29th appears among dates. The next step is to generate a relation with all of the month-day pairs that are under consideration; Since there's no easy way to do an interval check in terms of month-day, it's all happening using generate_series,
From there it's a simple matter of extracting the month and day from the target relation (the people alias) and filtering just the rows that are in the subselect.
SELECT *
FROM
(select column1 as birthdate, column2 as name
from (values
(date '1982-08-05', 'Alice'),
(date '1976-02-29', 'Bob'),
(date '1980-06-10', 'Carol'),
(date '1992-06-13', 'David')
) as birthdays) as people
WHERE
((EXTRACT(MONTH FROM people.birthdate),
EXTRACT(DAY FROM people.birthdate)) IN (
SELECT EXTRACT(MONTH FROM thedate.theday + s.a) AS m,
EXTRACT(DAY FROM thedate.theday + s.a) AS d
FROM
(SELECT date (v.column1 -
(extract (YEAR FROM v.column1)-2000) * INTERVAL '1 year'
) as theday
FROM (VALUES (date '2011-06-09')) as v) as thedate,
GENERATE_SERIES(0, 6) AS s(a)
)
)
Operating on days, as I've done here, should work splendidly all the way up until a two month interval (if you wanted to look out that far), since december 31 + two months and change should include the leap day. On the other hand, it's almost certainly more useful to just work on whole months for such a query, in which case you don't really need anything more than extract(month from ....
First find out how old the person currently is using age(), then grab the year from that extract(year from age()). This is how old they are currently in years, so for their age at their next birthday add 1 to the year. Then their next birthday is found by adding an interval of this many years * interval '1 year' to their birthday. Done.
I've used a subselect here to add the next_birth_day column in to the complete table to make the select clause simpler. You can then play with the where conditions to suit your needs.
select *
from (
select *,
(extract(year from age(birth_date)) + 1) * interval '1 year' + birth_date "next_birth_day"
from public.users
) as users_with_upcoming_birth_days
where next_birth_day between now() and now() + '7 days'
This is based on Daniel Lyons's anniversary idea, by calculating the interval between the next birthday and today, with just +/- date arithmetic:
SELECT
today,
birthday,
CASE
WHEN this_year_anniversary >= today
THEN this_year_anniversary
ELSE this_year_anniversary + '1 year'::interval
END - today < '1 week'::interval AS is_upcoming
FROM
(
SELECT
today,
birthday,
birthday + years AS this_year_anniversary
FROM
(
SELECT
today,
birthday,
((
extract(year FROM today) - extract(year from birthday)
) || ' years')::interval AS years
FROM
(VALUES ('2011-02-28'::date)) AS t1 (today),
(VALUES
('1975-02-28'::date),
('1975-03-06'::date),
('1976-02-28'::date),
('1976-02-29'::date),
('1976-03-06'::date)
) AS t2 (birthday)
) AS t
) AS t;
In case you want it to work with leap years:
create or replace function birthdate(date)
returns date
as $$
select (date_trunc('year', now()::date)
+ age($1, 'epoch'::date)
- (extract(year from age($1, 'epoch'::date)) || ' years')::interval
)::date;
$$ language sql stable strict;
Then:
where birthdate(birthdate) between current_date
and current_date + interval '1 week'
See also:
Getting all entries who's Birthday is today in PostgreSQL
Exemple: birthdate between: jan 20 and feb 10
SELECT * FROM users WHERE TO_CHAR(birthdate, '1800-MM-DD') BETWEEN '1800-01-20' AND '1800-02-10'
Why 1800?
No matter may be any year;
In my registration form, I can inform the date of birth (with years) or just the birthday (without year), in which case I saved as 1800 to make it easier to work with the date
Here's my take, which works with leap years too:
CREATE OR REPLACE FUNCTION days_until_birthday(
p_date date
) RETURNS integer AS $$
DECLARE
v_now date;
v_days integer;
v_date_upcoming date;
v_years integer;
BEGIN
v_now = now()::date;
IF (p_date IS NULL OR p_date > v_now) THEN
RETURN NULL;
END IF;
v_years = date_part('year', v_now) - date_part('year', p_date);
v_date_upcoming = p_date + v_years * interval '1 year';
IF (v_date_upcoming < v_now) THEN
v_date_upcoming = v_date_upcoming + interval '1 year';
END IF;
v_days = v_date_upcoming - v_now;
RETURN v_days;
END
$$ LANGUAGE plpgsql IMMUTABLE;
I know this post is old, but I had the same issue and came up with this simple and elegant solution:
It is pretty easy with age() and accounts for lap years... for the people who had their birthdays in the last 20 days:
SELECT * FROM c
WHERE date_trunc('year', age(birthdate)) != date_trunc('year', age(birthdate + interval '20 days'))
I have simply created this year date from original birth date.
( DATE_PART('month', birth_date) || '/' || DATE_PART('day', birth_date) || '/' || DATE_PART('year', now()))::date between :start_date and :end_date
I hope this help.
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
I want to find the LastWeek entries from postgres table with cycle from Monday to Sunday (both inclusive) For eg - if I query the data today i.e on 2020/07/26 (or say if i query data on any date between 2020/07/20 to 2020/07/26) i should get the data from 2020/07/13 to 2020/07/19
Query:
Select user, date_sent
from users
where date_sent between (SELECT current_date - cast(extract(dow from current_date) as int) - 6)
and (SELECT current_date - cast(extract(dow from current_date) as int) + 1)
Similarly I want to find the This Week entries week starting from Monday and ending on present date. For eg - If I query the data today i.e on 2020/07/26 I should get the data from 2020/07/20 to 2020/07/26. If i query on 2020/07/24 then I should get 2020/07/20 to 2020/07/24
Query:
select user, date_sent
from users
where date_sent >= date_trunc('week', current_date)
and date_sent <= date_trunc('day',current_date+1)
You are almost there.
For "this week":
select user, date_sent
from users
where date_sent >= date_trunc('week', current_date)
and date_sent < date_trunc('week', current_date) + interval '1 week';
For last week it's quite similar:
select user, date_sent
from users
where date_sent >= date_trunc('week', current_date) - interval '1 week'
and date_sent < date_trunc('week', current_date)
Your desired results are inconsistent. In your description, before your initial query you state:
if I query the data today i.e on 2020/07/26 (or say if i query data on
any date between 2020/07/20 to 2020/07/26) i should get the data from
2020/07/13 to 2020/07/19
But after that query you state:
If I query the data today i.e on 2020/07/26 I should get the data from
2020/07/20 to 2020/07/26.
You cannot have both.
Assuming the latter to be correct and assuming ISO-8601 week definition, then your request can be re-phased as:
Given a specified date, if that date falls in the same week as the
current date then return the dates from the start of the week to the
specified date, inclusive. If the specified date does not fall in the
current week return the dates return the dates from Monday on or prior
to the specified date through Sunday on or after the specified date, inclusive.
The following implements that.
with targets (for_week_containing_date
,from_week_start
,iso_from_week
,iso_this_week) as
( select &for_week_containing_date
, date_trunc('week', &for_week_containing_date)
, extract(week from &for_week_containing_date)
, extract(week from now())
)
select user, date_sent
from user_days
cross join targets
where 1=1
and date_sent >= from_week_start
and date_sent <= case when iso_from_week = iso_this_week
then for_week_containing_date
else from_week_start + interval '6 days'
end
;
Since I do not care much for substitution variables this would need bound variables from a script, or wrap wrap it in an SQL function. See example of that here. Also note the last 2 queries, make sure you are ok with and understand what's happening around year end. You may need to make end of year/ begin of year adjustments. The results are not from being in a function, but result from ISO-8601 definitions. End of year/Begin year checking is needed any time you deal with date ranges.
I’m building a booking system where a user will set their availability eg: I’m available Monday’s from 9am to 11am, Tuesdays from 9am to 5pm etc… and need to generate a list of time slots 15mins apart from their availability.
I have the following table (but am flexible to changing this):
availabilities(day_of_week text, start_time: time, end_time: time)
which returns records like:
‘Monday’ | 09:00:00 | 11:00:00
‘Monday’ | 13:00:00 | 17:00:00
‘Tuesday’ | 08:00:00 | 17:00:00
So I’m trying to build a stored procedure to generate a list of time slots so far I've got this:
create or replace function timeslots ()
return setof timeslots as $$
declare
rec record;
begin
for rec in select * from availabilities loop
/*
convert 'Monday' | 09:00:00 | 11:00:00 into:
2020-02-03 09:00:00
2020-02-03 09:15:00
2020-02-03 09:30:00
2020-02-03 09:45:00
2020-02-03 10:00:00
and so on...
*/
return next
end loop
$$ language plpgsql stable;
I return a setof instead of a table as I'm using Hasura and it needs to return a setof so I just create a blank table.
I think I'm on the right track but am currently stuck on:
how do I create a timestamp from 'Monday' 09:00:00 for the next monday as I only care about timeslots from today onwards?
how do I convert 'Monday' | 09:00:00 | 11:00:00 into a list of time slots 15 mins apart?
how do I create a timestamp from 'Monday' 09:00:00 for the next monday
as I only care about timeslots from today onwards?
You can use date_trunc for this (see this question for more info):
SELECT date_trunc('week', current_date) + interval '1 week';
From the docs re week:
The number of the ISO 8601 week-numbering week of the year. By
definition, ISO weeks start on Mondays
So taking this value and adding a week gives next Monday (you may need to ammend this behaviour based upon what you want to do if today is monday!).
how do I convert 'Monday' | 09:00:00 | 11:00:00 into a list of time
slots 15 mins apart?
This is a little tricker; generate_series will give you the timeslots but the trick is getting it into a result set. The following should do the job (I have included your sample data; change the values bit to refer to your table) - dbfiddle :
with avail_times as (
select
date_trunc('week', current_date) + interval '1 week' + case day_of_week when 'Monday' then interval '0 day' when 'Tuesday' then interval '1 day' end + start_time as start_time,
date_trunc('week', current_date) + interval '1 week' + case day_of_week when 'Monday' then interval '0 day' when 'Tuesday' then interval '1 day' end + end_time as end_time
from
(
values
('Monday','09:00:00'::time,'11:00:00'::time),
('Monday','13:00:00'::time,'17:00:00'::time),
('Tuesday','08:00:00'::time,'17:00:00'::time)
) as availabilities (day_of_week,
start_time,
end_time) )
select
g.ts
from
(
select
start_time,
end_time
from
avail_times) avail,
generate_series(avail.start_time, avail.end_time - interval '1ms', '15 minutes') g(ts);
A few notes:
The CTE avail_times is used to simplify things; it generates two columns (start_time and end_time) which are the full timestamps (so including the date). In this example the first row is "2020-02-03 09:00:00, 2020-02-03 11:00:00" (I'm running this on 2020-02-02 so 2020-02-03 is next Monday).
The way I'm converting 'monday' etc to a day of the week is a bit of a hack (and I have not bothered to do the full week); there is probably a better way but storing the day of week as an integer would make this simpler.
I subtract 1ms from the end time because I'm assuming you dont want this in the result set.
The main query is using a LATERAL Subquery. See this question for more info.
Aditional Question
how to adjust this so I can pass in a start and end date so I can get
time slots for a particular period
You could do something like the following (just adjust the dates CTE to return whatever days you want to include; you could convert to a function or just pass the dates in as parameters).
Note that as #Belayer mentions my original solution did not cater for shifts over midnight so this addresses that too.
with dates as (
select
day
from
generate_series('2020-02-20'::date, '2020-03-10'::date, '1 day') as day ),
availabilities as (
select
*
from
(
values (1,'09:00:00'::time,'11:00:00'::time),
(1,'13:00:00'::time,'17:00:00'::time),
(2,'08:00:00'::time,'17:00:00'::time),
(3,'23:00:00'::time,'01:00:00'::time)
) as availabilities
(day_of_week, -- 1 = monday
start_time,
end_time) ) ,
avail_times as (
select
d.day + start_time as start_time,
case
end_time > start_time
when true then d.day
else d.day + interval '1 day' end + end_time as end_time
from
availabilities a
inner join dates d on extract(ISODOW from d.day) = a.day_of_week )
select
g.ts
from
(
select
start_time,
end_time
from
avail_times) avail,
generate_series(avail.start_time, avail.end_time - interval '1ms', '15 minutes') g(ts)
order by
g.ts;
The following uses much of the techniques mentioned by #Brits. They present some very good information, so I'll not repeat but suggest you review it (and the links).
I do however take a slightly different approach. First a couple table changes. I use the ISO day of week 1-7 (Monday-Sunday) rather than the day name. The day name is easily extracted for the dater later.
Also I use interval instead to time for start and end times. ( A time data type works for most scenarios but there is one it doesn't (more later).
One thing your description does not make clear is whether the ending time is included it the available time or not. If included the last interval would be 11:00-11:15. If excluded the last interval is 10:45-11:00. I have assumed to excluded it. In the final results the end time is to be read as "up to but not including".
-- setup
create table availabilities (weekday integer, start_time interval, end_time interval);
insert into availabilities (weekday , start_time , end_time )
select wkday
, start_time
, end_time
from (select *
from (values (1, '09:00'::interval, '11:00'::interval)
, (1, '13:00'::interval, '17:00'::interval)
, (2, '08:00'::interval, '17:00'::interval)
, (3, '08:30'::interval, '10:45'::interval)
, (4, '10:30'::interval, '12:45'::interval)
) as v(wkday,start_time,end_time)
) r ;
select * from availabilities;
The Query
It begins with a CTE (next_week) generates a entry for each day of the week beginning Monday and the appropriate ISO day number for it. The main query joins these with the availabilities table to pick up times for matching days. Finally that result is cross joined with a generated timestamp to get the 15 minute intervals.
-- Main
with next_week (wkday,tm) as
(SELECT n+1, date_trunc('week', current_date) + interval '1 week' + n*interval '1 day'
from generate_series (0, 6) n
)
select to_char(gdtm,'Day'), gdtm start_time, gdtm+interval '15 min' end_time
from ( select wkday, tm, start_time, end_time
from next_week nw
join availabilities av
on (av.weekday = nw.wkday)
) s
cross join lateral
generate_series(start_time+tm, end_time+tm- interval '1 sec', interval '15 min') gdtm ;
The outlier
As mentioned there is one scenario where a time data type does not work satisfactory, but you may not nee it. What happens when a shift worker says they available time is 23:00-01:30. Believe me when a shift worker goes to work at 22:00 of Friday, 01:30 is still Friday night, even though the calendar might not agree. (I worked that shift for many years.) The following using interval handles that issue. Loading the same data as prior with an addition for the this case.
insert into availabilities (weekday, start_time, end_time )
select wkday
, start_time
, end_time + case when end_time < start_time
then interval '1 day'
else interval '0 day'
end
from (select *
from (values (1, '09:00'::interval, '11:00'::interval)
, (1, '13:00'::interval, '17:00'::interval)
, (2, '08:00'::interval, '17:00'::interval)
, (3, '08:30'::interval, '10:45'::interval)
, (5, '23:30'::interval, '02:30'::interval) -- Friday Night - Saturday Morning
) as v(wkday,start_time,end_time)
) r
;
select * from availabilities;
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);
How to calculate end of the month in Postgres? I have table with column date datatype. I want to calculate end of the month of every date. For Eg. In the table there values like "2015-07-10 17:52:51","2015-05-30 11:30:19" then end of the month should be like 31 July 2015,31 May 2015.
Please guide me in this.
How about truncating to the beginning of this month, jumping forward one month, then back one day?
=# select (date_trunc('month', now()) + interval '1 month - 1 day')::date;
date
------------
2015-07-31
(1 row)
Change now() to your date variable, which must be a timestamp, per the docs. You can then manipulate this output (with strftime, etc.) to any format you need.
Source
SELECT TO_CHAR(
DATE_TRUNC('month', CURRENT_DATE)
+ INTERVAL '1 month'
- INTERVAL '1 day',
'YYYY-MM-DD HH-MM-SS'
) endOfTheMonth
Hi I tried like this and it worked
Date(to_char(date_trunc('month'::text, msm013.msa011) + '1 mon - 1 day '::interval , 'DD-MON-YYYY') )
Thanks a lot!!