Related
I have the following date range:
begin: 2018-02-15
end: 2018-04-23
I want to achieve the following:
["2018-02-15 - 2018-02-28", "2018-03-01 - 2018-03-31", "2018-04-01 - 2018-04-23"]
Essentially, I want to divide a given date range into months. I can't think of a way to accomplish this in Python.
I have considered the solution here, however, this splits the date range based on a specified interval. I want to be able to split a date range dynamically.
Hence, given a date range from 15 February 2018 to 23 April 2018, I want to be able to get the individual months in the range, like so:
15 February 2018 to 28 February 2018
01 March 2018 to 31 March 2018
01 April 2018 to 23 April 2018
In a loop; starting at the first day continually add one day till you get to the end date; whenever the month changes save the dates.
import datetime
begin = '2018-02-15'
end = '2018-04-23'
dt_start = datetime.datetime.strptime(begin, '%Y-%m-%d')
dt_end = datetime.datetime.strptime(end, '%Y-%m-%d')
one_day = datetime.timedelta(1)
start_dates = [dt_start]
end_dates = []
today = dt_start
while today <= dt_end:
#print(today)
tomorrow = today + one_day
if tomorrow.month != today.month:
start_dates.append(tomorrow)
end_dates.append(today)
today = tomorrow
end_dates.append(dt_end)
out_fmt = '%d %B %Y'
for start, end in zip(start_dates,end_dates):
print('{} to {}'.format(start.strftime(out_fmt), end.strftime(out_fmt)))
Result:
>>>
15 February 2018 to 28 February 2018
01 March 2018 to 31 March 2018
01 April 2018 to 23 April 2018
>>>
You could probably figure out a way to get a range of months between the start and end dates; create a datetime object for the first day of each of those months store them and the days just prior to them. Dates spanning a change of year might be problematic though.
To work with convenient date objects, always use the standard module datetime. This wraps your string formatted dates, and allows easier calculations as well as tailored output formatting.
Unfortunately, it seems to miss one important piece of information: the last day of each month, given a year (which is necessary for Februari). There is an additional module calendar which returns the last day for a month, but since this is all you need of it and there is a simple datetime based function that does the same thing, I chose the latter.
With that, you can set any begin date and append it to your list, together with its last day of that month, then set begin to the next month's 1st and continue until you pass end.
A caveat/finetuning: I realized it would not work if both begin and end fall inside the same month. That needs an interim check, so I changed my initial while begin < end to while True and moved the check for crossing the end date into a separate line.
Also, to cross a year needs a separate test again, because else the statement month+1 will fail on December.
import datetime
# borrowed from https://stackoverflow.com/a/13565185
# as noted there, the calendar module has a function of its own
def last_day_of_month(any_day):
next_month = any_day.replace(day=28) + datetime.timedelta(days=4) # this will never fail
return next_month - datetime.timedelta(days=next_month.day)
begin = "2018-02-15"
end = "2018-04-23"
def monthlist(begin,end):
begin = datetime.datetime.strptime(begin, "%Y-%m-%d")
end = datetime.datetime.strptime(end, "%Y-%m-%d")
result = []
while True:
if begin.month == 12:
next_month = begin.replace(year=begin.year+1,month=1, day=1)
else:
next_month = begin.replace(month=begin.month+1, day=1)
if next_month > end:
break
result.append ([begin.strftime("%Y-%m-%d"),last_day_of_month(begin).strftime("%Y-%m-%d")])
begin = next_month
result.append ([begin.strftime("%Y-%m-%d"),end.strftime("%Y-%m-%d")])
return result
date_list = monthlist(begin,end)
print (date_list)
results in
[ ['2018-02-15', '2018-02-28'],
['2018-03-01', '2018-03-31'],
['2018-04-01', '2018-04-23'] ]
(slightly formatted for readability only)
If you don't mind using pandas, there's a nice helper date_range that will achieve what you want:
import pandas as pd
start = pd.Timestamp('20180215')
end = pd.Timestamp('20180423')
parts = list(pd.date_range(start, end, freq='M'))
# parts = [Timestamp('2018-02-28 00:00:00', freq='M'), Timestamp('2018-03-31 00:00:00', freq='M')]
if start != parts[0]:
parts.insert(0, start)
if end != parts[-1]:
parts.append(end)
parts[0] -= pd.Timedelta('1d') # we add back one day later
pairs = zip(map(lambda d: d + pd.Timedelta('1d'), parts[:-1]), parts[1:])
pairs_str = list(map(lambda t: t[0].strftime('%Y-%m-%d') + ' - ' + t[1].strftime('%Y-%m-%d'), pairs))
# pairs_str = ['2018-02-15 - 2018-02-28', '2018-03-01 - 2018-03-31', '2018-04-01 - 2018-04-23']
Using python calendar and accounting for change of the year
import calendar
from datetime import datetime
begin = '2018-02-15'
end= '2018-04-23'
begin_year, begin_month, begin_date = [int(i) for i in begin.split("-")]
end_year, end_month, end_date = [int(i) for i in end.split("-")]
years = end_year - begin_year
# if date range contains more than single year, we calculate total months
if years:
months = (12 - begin_month) + end_month + (12 * (years - 1))
else:
months = end_month - begin_month
dates = []
month = begin_month
year = begin_year
def create_datetime_object(y, m, d):
return datetime.strptime('{}-{}-{}'.format(y, m, d), '%Y-%m-%d')
# append the first date
dates.append(create_datetime_object(begin_year, begin_month, begin_date))
for i in range(months+1):
days_in_month = calendar.monthrange(year, month)[-1]
if month == begin_month and year == begin_year:
dates.append(create_datetime_object(begin_year, begin_month, days_in_month))
elif month == end_month and year == end_year:
dates.append(create_datetime_object(end_year, end_month, 1))
else:
dates.append(create_datetime_object(year, month, 1))
dates.append(create_datetime_object(year, month, days_in_month))
if month == 12:
month = 0
year += 1
month += 1
# append the last date
dates.append(create_datetime_object(end_year, end_month, end_date))
And to get a list in the question, we could do something like -
dates = [datetime.strftime(dt, '%Y-%m-%d') for dt in dates]
I had to do a similar manipulation and ended up building this function. I tested it on different use cases (different years, same month...) and it's working well.
It is inspired from S.Lott answer here
Creating a range of dates in Python
import datetime
def get_segments(start_date, end_date):
"""
Divides input date range into associated months periods
Example:
Input: start_date = 2018-02-15
end_date = 2018-04-23
Output:
["2018-02-15 - 2018-02-28",
"2018-03-01 - 2018-03-31",
"2018-04-01 - 2018-04-23"]
"""
curr_date = start_date
curr_month = start_date.strftime("%m")
segments = []
loop = (curr_date!=end_date)
days_increment = 1
while loop:
# Get incremented date with 1 day
curr_date = start_date + datetime.timedelta(days=days_increment)
# Get associated month
prev_month = curr_month
curr_month = curr_date.strftime("%m")
# Add to segments if new month
if prev_month!=curr_month:
# get start of segment
if not segments:
start_segment = start_date
else:
start_segment = segments[-1][1] + datetime.timedelta(days=1)
# get end of segment
end_segment = curr_date - datetime.timedelta(days=1)
# define and add segment
segment = [start_segment, end_segment]
segments.append(segment)
# stop if last day reached
loop = (curr_date!=end_date)
# increment added days
days_increment += 1
if not segments or segments[-1][1]!=end_date:
if not segments:
start_last_segment = start_date
else:
start_last_segment = segments[-1][1] + datetime.timedelta(days=1)
last_segment = [start_last_segment, end_date]
segments.append(last_segment)
for i in range(len(segments)):
segments[i][0] = segments[i][0].strftime("%Y-%m-%d")
segments[i][1] = segments[i][1].strftime("%Y-%m-%d")
return segments
Here is an example:
start_date = datetime.datetime(2020, 5, 27)
end_date = datetime.datetime(2021, 3, 1)
segments = get_segments(start_date, end_date)
for seg in segments:
print(seg)
Output:
['2020-05-27', '2020-05-31']
['2020-06-01', '2020-06-30']
['2020-07-01', '2020-07-31']
['2020-08-01', '2020-08-31']
['2020-09-01', '2020-09-30']
['2020-10-01', '2020-10-31']
['2020-11-01', '2020-11-30']
['2020-12-01', '2020-12-31']
['2021-01-01', '2021-01-31']
['2021-02-01', '2021-02-28']
['2021-03-01', '2021-03-01']
I extend the solution by #wwii
Now you will not have duplicate start and/or end dates
def date_range_split_monthly(begin, end):
dt_start = datetime.strptime(begin, '%Y-%m-%d')
dt_end = datetime.strptime(end, '%Y-%m-%d')
one_day = timedelta(1)
start_dates = [dt_start]
end_dates = []
today = dt_start
while today <= dt_end:
# print(today)
tomorrow = today + one_day
if tomorrow.month != today.month:
if tomorrow <= dt_end:
start_dates.append(tomorrow)
end_dates.append(today)
today = tomorrow
end_dates.append(dt_end)
return start_dates, end_dates
For people using Pendulum :
import pendulum
start = pendulum.now().subtract(months=6)
end = pendulum.today()
period = pendulum.period(start, end)
time_ranges = list(period.range("months"))
arr = []
for index, dt in enumerate(time_ranges):
if index < len(time_ranges) - 1:
start_range = time_ranges[index].format("YYYY-MM-D")
end_range = time_ranges[index + 1].format("YYYY-MM-D")
litt = F"{start_range} - {end_range}"
print(litt)
arr.append(litt)
print(arr)
More about period here
i'm quoting the comment of Kiran Subbaraman, just with addition of the exact keyword (otherwise, whole months will be returned even if ranges fall beyond the start or the end).
#!pip install arrow
from arrow import Arrow
Arrow.span_range('month', start, end, exact=True)
I need to subtract business days from the current date.
I currently have some code which needs always to be running on the most recent business day. So that may be today if we're Monday thru Friday, but if it's Saturday or Sunday then I need to set it back to the Friday before the weekend. I currently have some pretty clunky code to do this:
lastBusDay = datetime.datetime.today()
if datetime.date.weekday(lastBusDay) == 5: #if it's Saturday
lastBusDay = lastBusDay - datetime.timedelta(days = 1) #then make it Friday
elif datetime.date.weekday(lastBusDay) == 6: #if it's Sunday
lastBusDay = lastBusDay - datetime.timedelta(days = 2); #then make it Friday
Is there a better way?
Can I tell timedelta to work in weekdays rather than calendar days for example?
Use pandas!
import datetime
# BDay is business day, not birthday...
from pandas.tseries.offsets import BDay
today = datetime.datetime.today()
print(today - BDay(4))
Since today is Thursday, Sept 26, that will give you an output of:
datetime.datetime(2013, 9, 20, 14, 8, 4, 89761)
If you want to skip US holidays as well as weekends, this worked for me (using pandas 0.23.3):
import pandas as pd
from pandas.tseries.holiday import USFederalHolidayCalendar
from pandas.tseries.offsets import CustomBusinessDay
US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())
july_5 = pd.datetime(2018, 7, 5)
result = july_5 - 2 * US_BUSINESS_DAY # 2018-7-2
To convert to a python date object I did this:
result.to_pydatetime().date()
Maybe this code could help:
lastBusDay = datetime.datetime.today()
shift = datetime.timedelta(max(1,(lastBusDay.weekday() + 6) % 7 - 3))
lastBusDay = lastBusDay - shift
The idea is that on Mondays yo have to go back 3 days, on Sundays 2, and 1 in any other day.
The statement (lastBusDay.weekday() + 6) % 7 just re-bases the Monday from 0 to 6.
Really don't know if this will be better in terms of performance.
There seem to be several options if you're open to installing extra libraries.
This post describes a way of defining workdays with dateutil.
http://coding.derkeiler.com/Archive/Python/comp.lang.python/2004-09/3758.html
BusinessHours lets you custom-define your list of holidays, etc., to define when your working hours (and by extension working days) are.
http://pypi.python.org/pypi/BusinessHours/
DISCLAMER: I'm the author...
I wrote a package that does exactly this, business dates calculations. You can use custom week specification and holidays.
I had this exact problem while working with financial data and didn't find any of the available solutions particularly easy, so I wrote one.
Hope this is useful for other people.
https://pypi.python.org/pypi/business_calendar/
If somebody is looking for solution respecting holidays (without any huge library like pandas), try this function:
import holidays
import datetime
def previous_working_day(check_day_, holidays=holidays.US()):
offset = max(1, (check_day_.weekday() + 6) % 7 - 3)
most_recent = check_day_ - datetime.timedelta(offset)
if most_recent not in holidays:
return most_recent
else:
return previous_working_day(most_recent, holidays)
check_day = datetime.date(2020, 12, 28)
previous_working_day(check_day)
which produces:
datetime.date(2020, 12, 24)
timeboard package does this.
Suppose your date is 04 Sep 2017. In spite of being a Monday, it was a holiday in the US (the Labor Day). So, the most recent business day was Friday, Sep 1.
>>> import timeboard.calendars.US as US
>>> clnd = US.Weekly8x5()
>>> clnd('04 Sep 2017').rollback().to_timestamp().date()
datetime.date(2017, 9, 1)
In UK, 04 Sep 2017 was the regular business day, so the most recent business day was itself.
>>> import timeboard.calendars.UK as UK
>>> clnd = UK.Weekly8x5()
>>> clnd('04 Sep 2017').rollback().to_timestamp().date()
datetime.date(2017, 9, 4)
DISCLAIMER: I am the author of timeboard.
For the pandas usecase, I found the following to be quite useful and compact, although not completely readable:
Get most recent previous business day:
In [2]: datetime.datetime(2019, 11, 30) + BDay(1) - BDay(1) # Saturday
Out[2]: Timestamp('2019-11-29 00:00:00')
In [3]: datetime.datetime(2019, 11, 29) + BDay(1) - BDay(1) # Friday
Out[3]: Timestamp('2019-11-29 00:00:00')
In the other direction, simply use:
In [4]: datetime.datetime(2019, 11, 30) + BDay(0) # Saturday
Out[4]: Timestamp('2019-12-02 00:00:00')
In [5]: datetime.datetime(2019, 11, 29) + BDay(0) # Friday
Out[5]: Timestamp('2019-11-29 00:00:00')
This will give a generator of working days, of course without holidays, stop is datetime.datetime object. If you need holidays just make additional argument with list of holidays and check with 'IFology' ;-)
def workingdays(stop, start=datetime.date.today()):
while start != stop:
if start.weekday() < 5:
yield start
start += datetime.timedelta(1)
Later on you can count them like
workdays = workingdays(datetime.datetime(2015, 8, 8))
len(list(workdays))
def getNthBusinessDay(startDate, businessDaysInBetween):
currentDate = startDate
daysToAdd = businessDaysInBetween
while daysToAdd > 0:
currentDate += relativedelta(days=1)
day = currentDate.weekday()
if day < 5:
daysToAdd -= 1
return currentDate
When I am writing this answer, today is Friday in USA so next business day shall be Monday, in the meantime yesterday is thanksgiving holiday so previous business day should be Wednesday
So today date of Friday, November 24, 2022, is a perfect time to get the previous, current and next business days.
By having trial and error, I could only find the correct output by combining the method as below:
from datetime import datetime, timedelta
from pandas.tseries.offsets import BDay
from pandas.tseries.offsets import CustomBusinessDay
from pandas.tseries.holiday import USFederalHolidayCalendar
US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())
TODAY = datetime.today() - 1 * US_BUSINESS_DAY
YESTERDAY = (datetime.today() - timedelta(max(1,(TODAY.weekday() + 6) % 7 - 3))) - 1 * US_BUSINESS_DAY
TOMORROW = TODAY + BDay(1)
DAY_NAME = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday']
BUSINESS_DATE = "[Previous (" + DAY_NAME[YESTERDAY.weekday()] + "):'" + YESTERDAY.strftime('%y%m%d')
BUSINESS_DATE += "', Current (" + DAY_NAME[TODAY.weekday()] + "):'" + TODAY.strftime('%y%m%d')
BUSINESS_DATE += "', Next (" + DAY_NAME[TOMORROW.weekday()] + "):'" + TOMORROW.strftime('%y%m%d') + "']"
print_("Business Date USA = ", BUSINESS_DATE)
Output:
Business Date USA = [Previous (Wednesday):'221123', Current (Friday):'221125', Next (Monday):'221128']
Getting the most recent business day:
pd.bdate_range(end=(pd.to_datetime('today').date()), periods=1)[0])
OR in case you want it as a 'datetime.date' type:
(pd.bdate_range(end=(pd.to_datetime('today').date()), periods=1)[0]).date()
The accepted answer actually gives an incorrect result because today - BDay(0) rounds forward to Monday during the weekend instead of back to Friday like the question states. What you'd want is BusinessDay().rollback() which rolls back to the prior business day (the accepted answer matches BusinessDay().rollforward() logic).
import pandas as pd
import datetime
today = datetime.datetime.today()
prior_bday = pd.tseries.offsets.BusinessDay().rollback(today)
Why don't you try something like:
lastBusDay = datetime.datetime.today()
if datetime.date.weekday(lastBusDay) not in range(0,5):
lastBusDay = 5
another simplify version
lastBusDay = datetime.datetime.today()
wk_day = datetime.date.weekday(lastBusDay)
if wk_day > 4: #if it's Saturday or Sunday
lastBusDay = lastBusDay - datetime.timedelta(days = wk_day-4) #then make it Friday
Solution irrespective of different jurisdictions having different holidays:
If you need to find the right id within a table, you can use this snippet. The Table model is a sqlalchemy model and the dates to search from are in the field day.
def last_relevant_date(db: Session, given_date: date) -> int:
available_days = (db.query(Table.id, Table.day)
.order_by(desc(Table.day))
.limit(100).all())
close_dates = pd.DataFrame(available_days)
close_dates['delta'] = close_dates['day'] - given_date
past_dates = (close_dates
.loc[close_dates['delta'] < pd.Timedelta(0, unit='d')])
table_id = int(past_dates.loc[past_dates['delta'].idxmax()]['id'])
return table_id
This is not a solution that I would recommend when you have to convert in bulk. It is rather generic and expensive as you are not using joins. Moreover, it assumes that you have a relevant day that is one of the 100 most recent days in the model Table. So it tackles data input that may have different dates.
Get first day of month, last day of month and last business day of previous month if last day falls on weekend Saturday/Sunday
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
day = datetime(2023, 1, 10)
#last day of (n) previous month (n=months)
#n = 0 -- for current month
n=1
lastDayMonth = ((day - relativedelta(months=n) + relativedelta(day=31)).date());
#First day of previous month (n=months=1)
firstDayMonth = ((day - relativedelta(months=n) + relativedelta(day=1)).date());
print("Last Day of Month - "+ str(lastDayMonth))
print("First Day of Month - "+ str(firstDayMonth))
#Last business day (Friday) of prev (n) month (n=months=1)
lastBusDay = (lastDayMonth - timedelta(max(1,(lastDayMonth.weekday() + 6) % 7 - 3))) if lastDayMonth.weekday() in (5,6) else lastDayMonth
print("Last Business Day of Month - " + str(lastBusDay))
print()
--- Output
Last Day of Month - 2022-12-31
First Day of Month - 2022-12-01
Last Business Day of Month - 2022-12-30
Getting error for December month.
ValueError: month must be in 1..12
def last_day_of_month(ds):
cur_ds = datetime.strptime(ds, '%Y-%m-%d')
next_month = datetime(year=cur_ds.year, month=cur_ds.month+1, day=1)
last_day_month = next_month - timedelta(days=1)
return datetime.strftime(last_day_month, '%Y-%m-%d')
print last_day_of_month('2016-12-01')
In line 3 month=cur_ds.month+1 you are giving 13th month which is not valid. If you want to calculate last day of a given month you could also use month range from calendar library.
>>import calendar
>>year, month = 2016, 12
>>calendar.monthrange(year, month)[1]
31
You can't make a datetime with a month of 13. So you have to find a way to fix it. A simple solution is to convert the incremented month to an extra year:
# Reduce 12 to 1, 0 and all other #s to 0, #
extrayear, month = divmod(cur_ds.month, 12)
# Add 1 or 0 to existing year, add one to month (which was reduced to 0-11)
next_month = datetime(year=cur_ds.year + extrayear, month=month + 1, day=1)
You're passing in 12 as current month, then adding one to get next_month, making it 13. Check for the 12 case and set month=1 instead.
this is how I did it.
from django.utils import timezone
from calendar import monthrange
from datetime import datetime
current = timezone.now()
firstdayofmonth = current.replace(day=1)
endmonth = monthrange(current.year, current.month)
lastdayofmonth = datetime(current.year, current.month, endmonth[1])
I am trying to get the date delta by subtracting today's date from the nth day of the next month.
delta = nth_of_next_month - todays_date
print delta.days
How do you get the date object for the 1st (or 2nd, 3rd.. nth) day of the next month. I tried taking the month number from the date object and increasing it by 1. Which is obviously a dumb idea because 12 + 1 = 13. I also tried adding one month to today and tried to get to the first of the month. I am sure that there is a much more efficient way of doing this.
The dateutil library is useful for this:
from dateutil.relativedelta import relativedelta
from datetime import datetime
# Where day is the day you want in the following month
dt = datetime.now() + relativedelta(months=1, day=20)
This should be straightforward unless I'm missing something in your question:
import datetime
now = datetime.datetime.now()
nth_day = 5
next_month = now.month + 1 if now.month < 12 else 1 # February
year = now.year if now.month < 12 else now.year+1
nth_of_next_month = datetime.datetime(year, next_month, nth_day)
print(nth_of_next_month)
Result:
2014-02-05 00:00:00
Using dateutil as suggested in another answer is a much better idea than this, though.
Another alternative is to use delorean library:
Delorean is a library that provides easy and convenient datetime
conversions in Python.
>>> from delorean import Delorean
>>> d = Delorean()
>>> d.next_month()
Delorean(datetime=2014-02-15 18:51:14.325350+00:00, timezone=UTC)
>>> d.next_month().next_day(2)
Delorean(datetime=2014-02-17 18:51:14.325350+00:00, timezone=UTC)
My approach to calculating the next month without external libraries:
def nth_day_of_next_month(dt, n):
return dt.replace(
year=dt.year + (dt.month // 12), # +1 for december, +0 otherwise
month=(dt.month % 12) + 1, # december becomes january
day=n)
This works for both datetime.datetime() and datetime.date() objects.
Demo:
>>> import datetime
>>> def nth_day_of_next_month(dt, n):
... return dt.replace(year=dt.year + (dt.month // 12), month=(dt.month % 12) + 1, day=n)
...
>>> nth_day_of_next_month(datetime.datetime.now(), 4)
datetime.datetime(2014, 2, 4, 19, 20, 51, 177860)
>>> nth_day_of_next_month(datetime.date.today(), 18)
datetime.date(2014, 2, 18)
Without using any external library, this can be achived as follows
from datetime import datetime, timedelta
def nth_day_of_next_month(n):
today = datetime.now()
next_month_dt = today + timedelta(days=32-today.day)
return next_month_dt.replace(day=n)
I need to subtract business days from the current date.
I currently have some code which needs always to be running on the most recent business day. So that may be today if we're Monday thru Friday, but if it's Saturday or Sunday then I need to set it back to the Friday before the weekend. I currently have some pretty clunky code to do this:
lastBusDay = datetime.datetime.today()
if datetime.date.weekday(lastBusDay) == 5: #if it's Saturday
lastBusDay = lastBusDay - datetime.timedelta(days = 1) #then make it Friday
elif datetime.date.weekday(lastBusDay) == 6: #if it's Sunday
lastBusDay = lastBusDay - datetime.timedelta(days = 2); #then make it Friday
Is there a better way?
Can I tell timedelta to work in weekdays rather than calendar days for example?
Use pandas!
import datetime
# BDay is business day, not birthday...
from pandas.tseries.offsets import BDay
today = datetime.datetime.today()
print(today - BDay(4))
Since today is Thursday, Sept 26, that will give you an output of:
datetime.datetime(2013, 9, 20, 14, 8, 4, 89761)
If you want to skip US holidays as well as weekends, this worked for me (using pandas 0.23.3):
import pandas as pd
from pandas.tseries.holiday import USFederalHolidayCalendar
from pandas.tseries.offsets import CustomBusinessDay
US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())
july_5 = pd.datetime(2018, 7, 5)
result = july_5 - 2 * US_BUSINESS_DAY # 2018-7-2
To convert to a python date object I did this:
result.to_pydatetime().date()
Maybe this code could help:
lastBusDay = datetime.datetime.today()
shift = datetime.timedelta(max(1,(lastBusDay.weekday() + 6) % 7 - 3))
lastBusDay = lastBusDay - shift
The idea is that on Mondays yo have to go back 3 days, on Sundays 2, and 1 in any other day.
The statement (lastBusDay.weekday() + 6) % 7 just re-bases the Monday from 0 to 6.
Really don't know if this will be better in terms of performance.
There seem to be several options if you're open to installing extra libraries.
This post describes a way of defining workdays with dateutil.
http://coding.derkeiler.com/Archive/Python/comp.lang.python/2004-09/3758.html
BusinessHours lets you custom-define your list of holidays, etc., to define when your working hours (and by extension working days) are.
http://pypi.python.org/pypi/BusinessHours/
DISCLAMER: I'm the author...
I wrote a package that does exactly this, business dates calculations. You can use custom week specification and holidays.
I had this exact problem while working with financial data and didn't find any of the available solutions particularly easy, so I wrote one.
Hope this is useful for other people.
https://pypi.python.org/pypi/business_calendar/
If somebody is looking for solution respecting holidays (without any huge library like pandas), try this function:
import holidays
import datetime
def previous_working_day(check_day_, holidays=holidays.US()):
offset = max(1, (check_day_.weekday() + 6) % 7 - 3)
most_recent = check_day_ - datetime.timedelta(offset)
if most_recent not in holidays:
return most_recent
else:
return previous_working_day(most_recent, holidays)
check_day = datetime.date(2020, 12, 28)
previous_working_day(check_day)
which produces:
datetime.date(2020, 12, 24)
timeboard package does this.
Suppose your date is 04 Sep 2017. In spite of being a Monday, it was a holiday in the US (the Labor Day). So, the most recent business day was Friday, Sep 1.
>>> import timeboard.calendars.US as US
>>> clnd = US.Weekly8x5()
>>> clnd('04 Sep 2017').rollback().to_timestamp().date()
datetime.date(2017, 9, 1)
In UK, 04 Sep 2017 was the regular business day, so the most recent business day was itself.
>>> import timeboard.calendars.UK as UK
>>> clnd = UK.Weekly8x5()
>>> clnd('04 Sep 2017').rollback().to_timestamp().date()
datetime.date(2017, 9, 4)
DISCLAIMER: I am the author of timeboard.
For the pandas usecase, I found the following to be quite useful and compact, although not completely readable:
Get most recent previous business day:
In [2]: datetime.datetime(2019, 11, 30) + BDay(1) - BDay(1) # Saturday
Out[2]: Timestamp('2019-11-29 00:00:00')
In [3]: datetime.datetime(2019, 11, 29) + BDay(1) - BDay(1) # Friday
Out[3]: Timestamp('2019-11-29 00:00:00')
In the other direction, simply use:
In [4]: datetime.datetime(2019, 11, 30) + BDay(0) # Saturday
Out[4]: Timestamp('2019-12-02 00:00:00')
In [5]: datetime.datetime(2019, 11, 29) + BDay(0) # Friday
Out[5]: Timestamp('2019-11-29 00:00:00')
This will give a generator of working days, of course without holidays, stop is datetime.datetime object. If you need holidays just make additional argument with list of holidays and check with 'IFology' ;-)
def workingdays(stop, start=datetime.date.today()):
while start != stop:
if start.weekday() < 5:
yield start
start += datetime.timedelta(1)
Later on you can count them like
workdays = workingdays(datetime.datetime(2015, 8, 8))
len(list(workdays))
def getNthBusinessDay(startDate, businessDaysInBetween):
currentDate = startDate
daysToAdd = businessDaysInBetween
while daysToAdd > 0:
currentDate += relativedelta(days=1)
day = currentDate.weekday()
if day < 5:
daysToAdd -= 1
return currentDate
When I am writing this answer, today is Friday in USA so next business day shall be Monday, in the meantime yesterday is thanksgiving holiday so previous business day should be Wednesday
So today date of Friday, November 24, 2022, is a perfect time to get the previous, current and next business days.
By having trial and error, I could only find the correct output by combining the method as below:
from datetime import datetime, timedelta
from pandas.tseries.offsets import BDay
from pandas.tseries.offsets import CustomBusinessDay
from pandas.tseries.holiday import USFederalHolidayCalendar
US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())
TODAY = datetime.today() - 1 * US_BUSINESS_DAY
YESTERDAY = (datetime.today() - timedelta(max(1,(TODAY.weekday() + 6) % 7 - 3))) - 1 * US_BUSINESS_DAY
TOMORROW = TODAY + BDay(1)
DAY_NAME = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday','Sunday']
BUSINESS_DATE = "[Previous (" + DAY_NAME[YESTERDAY.weekday()] + "):'" + YESTERDAY.strftime('%y%m%d')
BUSINESS_DATE += "', Current (" + DAY_NAME[TODAY.weekday()] + "):'" + TODAY.strftime('%y%m%d')
BUSINESS_DATE += "', Next (" + DAY_NAME[TOMORROW.weekday()] + "):'" + TOMORROW.strftime('%y%m%d') + "']"
print_("Business Date USA = ", BUSINESS_DATE)
Output:
Business Date USA = [Previous (Wednesday):'221123', Current (Friday):'221125', Next (Monday):'221128']
Getting the most recent business day:
pd.bdate_range(end=(pd.to_datetime('today').date()), periods=1)[0])
OR in case you want it as a 'datetime.date' type:
(pd.bdate_range(end=(pd.to_datetime('today').date()), periods=1)[0]).date()
The accepted answer actually gives an incorrect result because today - BDay(0) rounds forward to Monday during the weekend instead of back to Friday like the question states. What you'd want is BusinessDay().rollback() which rolls back to the prior business day (the accepted answer matches BusinessDay().rollforward() logic).
import pandas as pd
import datetime
today = datetime.datetime.today()
prior_bday = pd.tseries.offsets.BusinessDay().rollback(today)
Why don't you try something like:
lastBusDay = datetime.datetime.today()
if datetime.date.weekday(lastBusDay) not in range(0,5):
lastBusDay = 5
another simplify version
lastBusDay = datetime.datetime.today()
wk_day = datetime.date.weekday(lastBusDay)
if wk_day > 4: #if it's Saturday or Sunday
lastBusDay = lastBusDay - datetime.timedelta(days = wk_day-4) #then make it Friday
Solution irrespective of different jurisdictions having different holidays:
If you need to find the right id within a table, you can use this snippet. The Table model is a sqlalchemy model and the dates to search from are in the field day.
def last_relevant_date(db: Session, given_date: date) -> int:
available_days = (db.query(Table.id, Table.day)
.order_by(desc(Table.day))
.limit(100).all())
close_dates = pd.DataFrame(available_days)
close_dates['delta'] = close_dates['day'] - given_date
past_dates = (close_dates
.loc[close_dates['delta'] < pd.Timedelta(0, unit='d')])
table_id = int(past_dates.loc[past_dates['delta'].idxmax()]['id'])
return table_id
This is not a solution that I would recommend when you have to convert in bulk. It is rather generic and expensive as you are not using joins. Moreover, it assumes that you have a relevant day that is one of the 100 most recent days in the model Table. So it tackles data input that may have different dates.
Get first day of month, last day of month and last business day of previous month if last day falls on weekend Saturday/Sunday
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
day = datetime(2023, 1, 10)
#last day of (n) previous month (n=months)
#n = 0 -- for current month
n=1
lastDayMonth = ((day - relativedelta(months=n) + relativedelta(day=31)).date());
#First day of previous month (n=months=1)
firstDayMonth = ((day - relativedelta(months=n) + relativedelta(day=1)).date());
print("Last Day of Month - "+ str(lastDayMonth))
print("First Day of Month - "+ str(firstDayMonth))
#Last business day (Friday) of prev (n) month (n=months=1)
lastBusDay = (lastDayMonth - timedelta(max(1,(lastDayMonth.weekday() + 6) % 7 - 3))) if lastDayMonth.weekday() in (5,6) else lastDayMonth
print("Last Business Day of Month - " + str(lastBusDay))
print()
--- Output
Last Day of Month - 2022-12-31
First Day of Month - 2022-12-01
Last Business Day of Month - 2022-12-30