Calendar function to also stop at the end of the month - python

I have this python function:
import datetime
def date_by_adding_business_days(from_date, add_days,holidays):
business_days_to_add = add_days
current_date = from_date
while business_days_to_add > 0:
current_date += datetime.timedelta(days=1)
weekday = current_date.weekday()
if weekday >= 5:
continue
if current_date in holidays:
continue
business_days_to_add -= 1
return current_date
Holidays =[datetime.datetime(2015,10,3),datetime.datetime(2015,10,4)]
print date_by_adding_business_days(datetime.date(2015,10,23), 6,Holidays)
This will output:
2015-11-02
What I want is to make a change that output date will stop at the end of the month, for my example I would need to output 2015-10-30. For any given date I don't want the output outside of the given date month. If I would have lets say: datetime.date(2015,10,29) and I want to add 5 days, this should output 2015-10-30 (only add 1 day) and so on. Holidays(marked in my list) and weekends aren't counted.
Thank you

You can use calendar.monthrange which returns tuple (first weekday of month, days in month) to figure out how many days there are in given month:
>>> import calendar
>>> calendar.monthrange(2015, 10)
(3, 31)
Then you can change your function to store the potential result every time the end of loop is being reached. On top of that you need to change the type of holidays to date instead of datetime since they are different objects:
>>> import datetime
>>> datetime.datetime(2016, 11, 17) == datetime.date(2016, 11, 17)
False
With above changes the end result would look something like this:
import datetime
import calendar
def date_by_adding_business_days(from_date, add_days,holidays):
business_days_to_add = add_days
current_date = from_date
result = None
_, days_in_month = calendar.monthrange(current_date.year, current_date.month)
while business_days_to_add > 0 and current_date.day < days_in_month:
current_date += datetime.timedelta(days=1)
weekday = current_date.weekday()
if weekday >= 5:
continue
if current_date in holidays:
continue
business_days_to_add -= 1
result = current_date
return result
Holidays =[datetime.date(2015,10,3),datetime.date(2015,10,4)]
print date_by_adding_business_days(datetime.date(2015,10,23), 6,Holidays)
Output:
2015-10-30

Related

Better way of incrementing a date month?

I have a date as an int like so for example: 201805 I'd like to have a way where I can give the start date and it'll return back the next date, in this example: 201806. Currently, I have this solution:
def incrementDate(startdate):
try:
newdate = dt.datetime.strptime(str(startdate + 1), "%Y%m")
return str(newdate.year) + newdate.strftime('%m')
except:
newdate = dt.datetime.strptime(str(startdate), "%Y%m")
return int(str(newdate.year + 1) + "01")
Is this a good way of going about this or is there a simple better way?
How about something using relativedelta:
Your first step will be to install the package dateutil:
pip install dateutil
Then you will be able to use it:
import datetime
from dateutil.relativedelta import relativedelta
print(datetime.datetime.strptime(str(201805), "%Y%m") + relativedelta(months=1))
Or a string:
print((datetime.datetime.strptime(str(201805), "%Y%m") + relativedelta(months=1)).strftime("%Y%m"))
Note: relativedelta comes from a separate package called 'dateutil' (here for the details). It includes notably a nice way to add month / days / year .. without creating a custom function.
You can use datetime.timedelta:
from datetime import datetime, timedelta
def incrementDate(startdate):
cur_date = datetime.strptime(str(startdate), '%Y%m')
next_date = cur_date + timedelta(days=32)
return datetime.strftime(next_date, '%Y%m')
print(incrementDate(201806)) # 201807
print(incrementDate(201812)) # 201901
Just for fun, if your input is really an integer that represents a 4 digit year folowed by a 2 digit month, then you can increment the month without any string or datetime conversions.
d = 201812
y, m = d // 100, d % 100
if m == 12:
y += 1
m = 1
else:
m +=1
d = y * 100 + m
print(d)
# 201806
The replace() method fron the datetime objects allows you to update a parameter from the datetime object:
import datetime
today = datetime.datetime.now()
current_month = today.month
year = today.year
# Check for december
if current_month < 12:
next_month = current_month + 1
else:
next_month = 1
year += 1
today= today.replace(month = next_month, year = year )
Reference
To put in in your function:
import datetime
def increment_date(startdate):
date = datetime.strptime(str(startdate), '%Y%m')
current_month = date .month
year = date .year
# Check for december
if current_month < 12:
next_month = current_month + 1
else:
next_month = 1
year += 1
return date .replace(month = next_month, year = year )

How to find number of Mondays or any other weekday between two dates in Python?

I have two dates between which I need to find out how many Mon- Fri are coming(except for Sta, Sun), everyday should be counted
Currently I am thinking this:
import calendar
import datetime
start_date = datetime.datetime.strptime("01/01/2017",'%d/%m/%Y')
end_date = datetime.datetime.strptime("31/01/2017",'%d/%m/%Y')
week_arr = [0] * 7
calendar.day_name[start_date.weekday()] ## will give me name of day
"""
As I receive Monday I will increment week_arr[0] by 1, Tuesday
week_arr[1]+= 1,
"""
I am not getting how to do it effectively so that I dont use much line of code(less if -else and for loops), may be some tricks in pandas.
You can define a function and use it like this :
def num_days_between( start, end, week_day):
num_weeks, remainder = divmod( (end-start).days, 7)
if ( week_day - start.weekday() ) % 7 < remainder:
return num_weeks + 1
else:
return num_weeks
where week_day is day number you wan to calculate count.
This code still uses a for loop and an if/else.
import datetime
import calendar
def weekday_count(start, end):
start_date = datetime.datetime.strptime(start, '%d/%m/%Y')
end_date = datetime.datetime.strptime(end, '%d/%m/%Y')
week = {}
for i in range((end_date - start_date).days):
day = calendar.day_name[(start_date + datetime.timedelta(days=i+1)).weekday()]
week[day] = week[day] + 1 if day in week else 1
return week
print(weekday_count("01/01/2017", "31/01/2017"))
# prints result
# {'Monday': 5, 'Tuesday': 5, 'Friday': 4, 'Wednesday': 4, 'Thursday': 4, 'Sunday': 5, 'Saturday': 4}
Number of Mondays in 2020 can be got using numpy library
import numpy as np
np.busday_count('2020', '2021', weekmask='Mon')
This is efficient - even in the face of ten thousands of days between start and end - and still very flexible (it iterates at most 7 times inside the sum function):
def intervening_weekdays(start, end, inclusive=True, weekdays=[0, 1, 2, 3, 4]):
if isinstance(start, datetime.datetime):
start = start.date() # make a date from a datetime
if isinstance(end, datetime.datetime):
end = end.date() # make a date from a datetime
if end < start:
# you can opt to return 0 or swap the dates around instead
raise ValueError("start date must be before end date")
if inclusive:
end += datetime.timedelta(days=1) # correct for inclusivity
try:
# collapse duplicate weekdays
weekdays = {weekday % 7 for weekday in weekdays}
except TypeError:
weekdays = [weekdays % 7]
ref = datetime.date.today() # choose a reference date
ref -= datetime.timedelta(days=ref.weekday()) # and normalize its weekday
# sum up all selected weekdays (max 7 iterations)
return sum((ref_plus - start).days // 7 - (ref_plus - end).days // 7
for ref_plus in
(ref + datetime.timedelta(days=weekday) for weekday in weekdays))
This takes both datetime.date as well as datetime.datetime objects for start and end, respectively.
Also, you can choose between a closed (inclusive=True) and a half-open (inclusive=False) interval.
By default, it calculates the number of workdays between the dates, but you can choose any set of weekdays (weekend days: weekdays=[5, 6]) or single weekdays (Wednesdays: weekdays=2) as well.
If anyone need an even simpler answer,
from datetime import date
d1 = date(2017, 1, 4)
d2 = date(2017, 1, 31)
count = 0
for d_ord in range(d1.toordinal(), d2.toordinal()):
d = date.fromordinal(d_ord)
if (d.weekday() == 4):
count += 1
print(count)
I found a simple and easy to understand code using for loop.
Take first date identify its weekday with "%a" compare it with your intrested weekday if found increment a count value. and repeat the steps till your last day.
Code is as below for your refrence i took monday as my intrested weekdays.
import datetime
A1=datetime.datetime.strptime("1/23/2016", "%m/%d/%Y")
A2=datetime.datetime.strptime("11/10/2016", "%m/%d/%Y")
count=0
week="Mon"
for i in range ((A2-A1).days): #gives the no of days from A1 to A2
if A1.strftime("%a")==week:
count+=1
A1+=datetime.timedelta(days=1)
print(count)
https://www.w3schools.com/python/python_datetime.asp

Generate date ranges broken by month for a given period

I'm struggling with writing a pythonic, clean generator method that, given a date period, like ['2014-01-15', '2015-02-03], will give me this:
['2014-01-15', '2014-01-31']
['2014-02-01', '2014-02-28']
...
['2015-02-01', '2015-02-03']
This is what I came up with:
from datetime import datetime
import calendar
def genDatePeriods(startDate, endDate, format='%Y-%m-%d'):
dt1 = datetime.strptime(startDate, format)
dt2 = datetime.strptime(endDate, format)
for year in range(dt1.year, dt2.year + 1):
for month in range(1, 13):
day0 = dt1.day if month == dt1.month and year == dt1.year else 1
day1 = dt2.day if month == dt2.month and year == dt2.year else calendar.monthrange(year, month)[1]
if (year == dt1.year and month < dt1.month) or (year == dt2.year and month > dt2.month):
continue
else:
d0 = (year, month, day0)
d1 = (year, month, day1)
yield [datetime(*d).strftime(format) for d in [d0, d1]]
It works, however I feel like there is a more pythonic/tidy/efficient way to do this. Any ideas?
The following is much more concise, using datetime.date() objects to find the first day of the next month each time, until you reach the end date:
from datetime import datetime, timedelta
def genDatePeriods(startDate, endDate, format='%Y-%m-%d'):
curr = datetime.strptime(startDate, format).date()
end = datetime.strptime(endDate, format).date()
while curr <= end:
# first day of the next month, using modular arithmetic
next_month = curr.replace(
month=curr.month % 12 + 1, year=curr.year + curr.month // 12,
day=1)
curr_formatted = curr.strftime(format)
# end date is next month's first day, minus one day,
# or the given endDate, whichever comes first
end_formatted = min(next_month - timedelta(days=1), end).strftime(format)
yield [curr_formatted, end_formatted]
curr = next_month
Demo:
>>> for res in genDatePeriods('2014-01-15', '2015-02-03'):
... print res
...
['2014-01-15', '2014-01-31']
['2014-02-01', '2014-02-28']
['2014-03-01', '2014-03-31']
['2014-04-01', '2014-04-30']
['2014-05-01', '2014-05-31']
['2014-06-01', '2014-06-30']
['2014-07-01', '2014-07-31']
['2014-08-01', '2014-08-31']
['2014-09-01', '2014-09-30']
['2014-10-01', '2014-10-31']
['2014-11-01', '2014-11-30']
['2014-12-01', '2014-12-31']
['2015-01-01', '2015-01-31']
['2015-02-01', '2015-02-03']

How can I get the 3rd Friday of a month in Python?

I'm trying to get stock data from Yahoo! Finance using Python 2.7.9, but I only need data for the 3rd Friday of the month. I have a function to get the data, but need a way to get the dates. I want something like this:
def get_third_fris(how_many):
# code and stuff
return list_of_fris
So that calling get_third_fris(6) will return a 6-item-long list of 3rd Fridays following the current date. The dates need to be Unix timestamps.
(I have pretty much no experience with time or datetime, so please explain what your code is doing.)
Thanks!
You can use the calendar module to list weeks, then grab the Friday of that week.
import calendar
c = calendar.Calendar(firstweekday=calendar.SUNDAY)
year = 2015; month = 2
monthcal = c.monthdatescalendar(year,month)
third_friday = [day for week in monthcal for day in week if \
day.weekday() == calendar.FRIDAY and \
day.month == month][2]
You can format to Unix timestamp, but it's non-trivial. I'll refer you to this excellent answer which has info based on whether or not your date is timezone-aware.
We do not need to import anything other than datetime. We can assume 7 days in a week and weekday 0 == Monday.
import datetime
def third_friday(year, month):
"""Return datetime.date for monthly option expiration given year and
month
"""
# The 15th is the lowest third day in the month
third = datetime.date(year, month, 15)
# What day of the week is the 15th?
w = third.weekday()
# Friday is weekday 4
if w != 4:
# Replace just the day (of month)
third = third.replace(day=(15 + (4 - w) % 7))
return third
Assuming you want a range of every 3rd Friday, you can just use pandas, sample code:
import pandas as pd
pd.date_range('2017-12-02','2020-08-31',freq='WOM-3FRI')
Output:
DatetimeIndex(['2017-12-15', '2018-01-19', '2018-02-16', '2018-03-16',
'2018-04-20', '2018-05-18', '2018-06-15', '2018-07-20',
'2018-08-17', '2018-09-21', '2018-10-19', '2018-11-16',
'2018-12-21', '2019-01-18', '2019-02-15', '2019-03-15',
'2019-04-19', '2019-05-17', '2019-06-21', '2019-07-19',
'2019-08-16', '2019-09-20', '2019-10-18', '2019-11-15',
'2019-12-20', '2020-01-17', '2020-02-21', '2020-03-20',
'2020-04-17', '2020-05-15', '2020-06-19', '2020-07-17',
'2020-08-21'],
dtype='datetime64[ns]', freq='WOM-3FRI')
You can use standard python functions to find the third friday of this month:
from datetime import timedelta, date
import calendar
def next_third_friday(d):
""" Given a third friday find next third friday"""
d += timedelta(weeks=4)
return d if d.day >= 15 else d + timedelta(weeks=1)
def third_fridays(d, n):
"""Given a date, calculates n next third fridays"""
# Find closest friday to 15th of month
s = date(d.year, d.month, 15)
result = [s + timedelta(days=(calendar.FRIDAY - s.weekday()) % 7)]
# This month's third friday passed. Find next.
if result[0] < d:
result[0] = next_third_friday(result[0])
for i in range(n - 1):
result.append(next_third_friday(result[-1]))
return result
We can apply the above function to get the timestamps of the next fridays:
import time
def timestamp(d):
return int(time.mktime(d.timetuple()))
fridays = third_fridays(date.today(), 2)
print(fridays)
print(map(timestamp, fridays))
Output:
[datetime.date(2015, 3, 20), datetime.date(2015, 4, 17)]
[1426802400, 1429218000]
How about a more straightforward answer:
import calendar
c = calendar.Calendar(firstweekday=calendar.SATURDAY)
monthcal = c.monthdatescalendar(my_year, my_month)
monthly_expire_date = monthcal[2][-1]
I generalized #pourhaus answer to find the nth day of any month:
def nth_day_of_month(month, year, day_of_week, n):
first_possible_day = {1: 1, 2: 8, 3: 15, 4: 22, 5: 29}[n]
d = datetime.date(year, month, first_possible_day)
w = d.weekday()
if w != day_of_week:
d = d.replace(day=(first_possible_day + (day_of_week - w) % 7))
return d
its easy to use dateutil to get the next friday
import dateutil.parser as dparse
from datetime import timedelta
next_friday = dparse.parse("Friday")
one_week = timedelta(days=7)
friday_after_next = next_friday + one_week
last_friday = friday_after_next + one_week
this leverages the fact that there is always a week between fridays ... although Im not sure this answers your question it should at the very least provide you with a good starting point
Using dateutil.relativedelta:
from dateutil.relativedelta import relativedelta, FR # $ pip install python-dateutil
def third_friday_dateutil(now):
"""the 3rd Friday of the month, not the 3rd Friday after today."""
now = now.replace(day=1) # 1st day of the month
now += relativedelta(weeks=2, weekday=FR)
return now
Or using dateutil.rrule:
from datetime import date, timedelta
from dateutil.rrule import rrule, MONTHLY, FR
def third_friday_rrule(now):
return rrule(MONTHLY, count=1, byweekday=FR, bysetpos=3, dtstart=now.replace(day=1))[0]
def get_third_fris_rrule(how_many):
return list(rrule(MONTHLY, count=how_many, byweekday=FR, bysetpos=3, dtstart=date.today()+timedelta(1)))
Here's a brute force solution (15x times faster):
#!/usr/bin/env python
import calendar
from datetime import date, timedelta
from itertools import islice
DAY = timedelta(1)
WEEK = 7*DAY
def fridays(now):
while True:
if now.weekday() == calendar.FRIDAY:
while True:
yield now
now += WEEK
now += DAY
def next_month(now):
"""Return the first date that is in the next month."""
return (now.replace(day=15) + 20*DAY).replace(day=1)
def third_friday_brute_force(now):
"""the 3rd Friday of the month, not the 3rd Friday after today."""
return next(islice(fridays(now.replace(day=1)), 2, 3))
def get_third_fris(how_many):
result = []
now = date.today()
while len(result) < how_many:
fr = third_friday_brute_force(now)
if fr > now: # use only the 3rd Friday after today
result.append(fr)
now = next_month(now)
return result
print(get_third_fris(6))
Output
[datetime.date(2015, 3, 20),
datetime.date(2015, 4, 17),
datetime.date(2015, 5, 15),
datetime.date(2015, 6, 19),
datetime.date(2015, 7, 17),
datetime.date(2015, 8, 21)]
See Converting datetime.date to UTC timestamp in Python
Here's comparison with other solutions and tests (for all possible 400 years patterns).
I generalized my answer so that anyone can use it for any Nth weekday of a month and using minimal default libraries. My use was to find the DST (daylight savings time) dates for the year (2nd sunday in March & 1st sunday in November).
# Libraries:
from datetime import datetime
# Function:
def get_nth_day_of_month(year, month, Nth, weekday):
# Process is to find out what weekday the 1st of the month is
# And then go straight to the desired date by calculating it
first_of_month_weekday = datetime(year, month, 1).weekday()
day_desired = 7 * (Nth-1) + (weekday - first_of_month_weekday)
if day_desired < 1 : day_desired += 7 #correction for some 1st-weekday situations
return datetime(year, month, day_desired)
# Config:
year = 2022
month = 3 #DST starts in March
weekday = 6 #sunday
Nth = 2 #2nd sunday
dst_start = get_nth_day_of_month(year, month, Nth, weekday)
For my case, this generates the start of DST this year:
In [2]: dst_start
Out [2]: datetime.datetime(2022, 3, 13, 0, 0)
Then for the end of DST in 2022:
month = 11
Nth = 1
dst_end = get_nth_day_of_month(year, month, Nth, weekday)
The result is:
In[4]: dst_end
Out[4]: datetime.datetime(2022, 11, 5, 0, 0)
So in 2022, DST runs from 2022-03-13 to 2022-11-05.
Standard:
Days are numbered Monday = 0 to Sunday = 6
Pure python with no external libs.
Returns the expected day-of-month.
Note: Based on answer from #autonopy, but works.
from datetime import datetime
def get_nth_day_of_month(year, month, Nth, weekday):
first_of_month_weekday = datetime(year, month, 1).weekday()
# Find weekday offset from beginning of month
day_offset = (weekday - first_of_month_weekday) + 1
if day_offset < 1:
day_offset += 7 # correction for some 1st-weekday situations
# Add N weeks
return 7 * (Nth - 1) + day_offset
Tests:
>>> # first Monday of Nov 2021
>>> get_nth_day_of_month(2021, 11, 1, 0)
1
>>> # first Monday of January 2022
>>> get_nth_day_of_month(2022, 1, 1, 0)
3
>>> # first Monday of May 2022
>>> get_nth_day_of_month(2022, 5, 1, 0)
2
>>> # Mother's day 2022
>>> get_nth_day_of_month(2022, 5, 2, 0)
9
Assuming you use pandas:
def exp_friday(df):
mask = np.where((df.index.day > 14) &
(df.index.day < 22) &
(df.index.dayofweek == 4), True, False)
return df[mask]
This is a generic function to give you all the dates of a specific week in a list form.
def frecuencia_daymng(self, start_day, year, month, dayofweek):
"""dayofweek starts on MONDAY in 0 index"""
c = calendar.Calendar(firstweekday=start_day)
monthcal = c.monthdatescalendar(year, month)
ldates = []
for tdate in monthcal:
if tdate[dayofweek].month == month:
ldates.append(tdate[dayofweek])
return ldates
Lets say you want all the mondays of the 2020 10.
frecuencia_daymng(calendar.MONDAY, 2020, 10, 0)
This will give you the output.
[datetime.date(2020, 10, 5),
datetime.date(2020, 10, 12),
datetime.date(2020, 10, 19),
datetime.date(2020, 10, 26)]
So now you have the first, second ... etc monday of the month.
My suggestion is to start with the first day of the month, then find the closest Friday.
4 is represented as Friday from the datetime.weekday() method.
So we then subtract the weekday of the first of the month from 4(Friday)
If the result is negative the closest Friday found was the previous month, so we add 7 days, otherwise we already have the first Friday.
Then the result is as simple as adding another 14 days to get the third Friday and then add the timedelta representing the third Friday to the first day of the month.
from datetime import datetime, timedelta
def get_third_friday(year, month):
first_day_of_month = datetime(year, month, 1)
closest_friday = 4 - first_day_of_month.weekday()
if closest_friday < 0:
first_friday = closest_friday + 7
else:
first_friday = closest_friday
third_friday = first_friday + 14
return first_day_of_month + timedelta(third_friday)
Here's a solution where someone has figured it out already: the relativedelta module that's an extension from the Python dateutil package (pip install python-dateutil).
import datetime
from dateutil import relativedelta
def third_fridays(n):
first_of_this_month = datetime.date.today().replace(day=1)
return (
first_of_this_month
+ relativedelta.relativedelta(weekday=relativedelta.FR(3), months=i)
for i in range(n)
)
The key part here of course is the weekday=relativedelta.FR(3) which says exactly what's needed: the third Friday of the month. Here are the relevant part of the docs for the weekday parameter,
weekday:
One of the weekday instances (MO, TU, etc) available in the
relativedelta module. These instances may receive a parameter N,
specifying the Nth weekday, which could be positive or negative
(like MO(+1) or MO(-2)).
(For those new to Python return (...) is a generator expression which you can just treat as something to iterate over, e.g., for friday in third_fridays(18): print(friday))
from dateutil.relativedelta import *
from datetime import *
def find_mth_friday(your_date,m):
mth_friday = your_date + relativedelta(day=1, weekday=FR(m)) #sets day=1 in your_date and adds m fridays to it.
mth_friday_timestamp = int(mth_friday.strftime("%s")) #converting datetime to unix timestamp
return mth_friday_timestamp
def get_third_fris(n):
output_timestamps = []
today = datetime.now() #gets current system date
for i in range(1,n+1): #value of i varies from 1 to 6 if n=6
next_month = today + relativedelta(months=+i) #adds i months to current system date
third_friday = find_mth_friday(next_month,3) #finds third friday of the month using 'find_mth_friday()', the function we defined
output_timestamps.append(third_friday)
return output_timestamps
print(get_third_fris(6)) #let's try invoking our function with n=6 dates
This is what you wanted right?

Add n business days to a given date ignoring holidays and weekends in python

I'm trying to add n (integer) working days to a given date, the date addition has to avoid the holidays and weekends (it's not included in the working days)
Skipping weekends would be pretty easy doing something like this:
import datetime
def date_by_adding_business_days(from_date, add_days):
business_days_to_add = add_days
current_date = from_date
while business_days_to_add > 0:
current_date += datetime.timedelta(days=1)
weekday = current_date.weekday()
if weekday >= 5: # sunday = 6
continue
business_days_to_add -= 1
return current_date
#demo:
print '10 business days from today:'
print date_by_adding_business_days(datetime.date.today(), 10)
The problem with holidays is that they vary a lot by country or even by region, religion, etc. You would need a list/set of holidays for your use case and then skip them in a similar way. A starting point may be the calendar feed that Apple publishes for iCal (in the ics format), the one for the US would be http://files.apple.com/calendars/US32Holidays.ics
You could use the icalendar module to parse this.
If you don't mind using a 3rd party library then dateutil is handy
from dateutil.rrule import *
print "In 4 business days, it's", rrule(DAILY, byweekday=(MO,TU,WE,TH,FR))[4]
You can also look at rruleset and using .exdate() to provide the holidays to skip those in the calculation, and optionally there's a cache option to avoid re-calculating that might be worth looking in to.
There is no real shortcut to do this. Try this approach:
Create a class which has a method skip(self, d) which returns True for dates that should be skipped.
Create a dictionary in the class which contains all holidays as date objects. Don't use datetime or similar because the fractions of a day will kill you.
Return True for any date that is in the dictionary or d.weekday() >= 5
To add N days, use this method:
def advance(d, days):
delta = datetime.timedelta(1)
for x in range(days):
d = d + delta
while holidayHelper.skip(d):
d = d + delta
return d
Thanks based on omz code i made some little changes ...it maybe helpful for other users:
import datetime
def date_by_adding_business_days(from_date, add_days,holidays):
business_days_to_add = add_days
current_date = from_date
while business_days_to_add > 0:
current_date += datetime.timedelta(days=1)
weekday = current_date.weekday()
if weekday >= 5: # sunday = 6
continue
if current_date in holidays:
continue
business_days_to_add -= 1
return current_date
#demo:
Holidays =[datetime.datetime(2012,10,3),datetime.datetime(2012,10,4)]
print date_by_adding_business_days(datetime.datetime(2012,10,2), 10,Holidays)
I wanted a solution that wasn't O(N) and it looked like a fun bit of code golf. Here's what I banged out in case anyone's interested. Works for positive and negative numbers. Let me know if I missed anything.
def add_business_days(d, business_days_to_add):
num_whole_weeks = business_days_to_add / 5
extra_days = num_whole_weeks * 2
first_weekday = d.weekday()
remainder_days = business_days_to_add % 5
natural_day = first_weekday + remainder_days
if natural_day > 4:
if first_weekday == 5:
extra_days += 1
elif first_weekday != 6:
extra_days += 2
return d + timedelta(business_days_to_add + extra_days)
I know it does not handle holidays, but I found this solution more helpful because it is constant in time. It consists of counting the number of whole weeks, adding holidays is a little more complex. I hope it can help somebody :)
def add_days(days):
today = datetime.date.today()
weekday = today.weekday() + ceil(days)
complete_weeks = weekday // 7
added_days = weekday + complete_weeks * 2
return today + datetime.timedelta(days=added_days)
This will take some work since there isn't any defined construct for holidays in any library (by my knowledge at least). You will need to create your own enumeration of those.
Checking for weekend days is done easily by calling .weekday() < 6 on your datetime object.
Refactoring omz code, and using holidays package, this is what I use to add business days taking into account the country's holidays
import datetime
import holidays
def today_is_holiday(date):
isHoliday = date.date() in [key for key in holidays.EN(years = date.year).keys()]
isWeekend = date.weekday() >= 5
return isWeekend or isHoliday
def date_by_adding_business_days(from_date, add_days):
business_days_to_add = add_days
current_date = from_date
while business_days_to_add > 0:
current_date += datetime.timedelta(days=1)
if today_is_holiday(current_date):
continue
business_days_to_add -= 1
return current_date
Hope this helps. It's not O(N) but O(holidays). Also, holidays only works when the offset is positive.
def add_working_days(start, working_days, holidays=()):
"""
Add working_days to start start date , skipping weekends and holidays.
:param start: the date to start from
:type start: datetime.datetime|datetime.date
:param working_days: offset in working days you want to add (can be negative)
:type working_days: int
:param holidays: iterator of datetime.datetime of datetime.date instances
:type holidays: iter(datetime.date|datetime.datetime)
:return: the new date wroking_days date from now
:rtype: datetime.datetime
:raise:
ValueError if working_days < 0 and holidays
"""
assert isinstance(start, (datetime.date, datetime.datetime)), 'start should be a datetime instance'
assert isinstance(working_days, int)
if working_days < 0 and holidays:
raise ValueError('Holidays and a negative offset is not implemented. ')
if working_days == 0:
return start
# first just add the days
new_date = start + datetime.timedelta(working_days)
# now compensate for the weekends.
# the days is 2 times plus the amount of weeks are included in the offset added to the day of the week
# from the start. This compensates for adding 1 to a friday because 4+1 // 5 = 1
new_date += datetime.timedelta(2 * ((working_days + start.weekday()) // 5))
# now compensate for the holidays
# process only the relevant dates so order the list and abort the handling when the holiday is no longer
# relevant. Check each holiday not being in a weekend, otherwise we don't mind because we skip them anyway
# next, if a holiday is found, just add 1 to the date, using the add_working_days function to compensate for
# weekends. Don't pass the holiday to avoid recursion more then 1 call deep.
for hday in sorted(holidays):
if hday < start:
# ignore holidays before start, we don't care
continue
if hday.weekday() > 4:
# skip holidays in weekends
continue
if hday <= new_date:
# only work with holidays up to and including the current new_date.
# increment using recursion to compensate for weekends
new_date = add_working_days(new_date, 1)
else:
break
return new_date
If someone needs to add/substract days, extending #omz's answer:
def add_business_days(from_date, ndays):
business_days_to_add = abs(ndays)
current_date = from_date
sign = ndays/abs(ndays)
while business_days_to_add > 0:
current_date += datetime.timedelta(sign * 1)
weekday = current_date.weekday()
if weekday >= 5: # sunday = 6
continue
business_days_to_add -= 1
return current_date
similar to #omz solution but recursively:
def add_days_skipping_weekends(start_date, days):
if not days:
return start_date
start_date += timedelta(days=1)
if start_date.weekday() < 5:
days -= 1
return add_days_skipping_weekends(start_date, days)
If you are interested in using NumPy, then you can follow the solution below:
import numpy as np
from datetime import datetime, timedelta
def get_future_date_excluding_weekends(date,no_of_days):
"""This methods return future date by adding given number of days excluding
weekends"""
future_date = date + timedelta(no_of_days)
no_of_busy_days = int(np.busday_count(date.date(),future_date.date()))
if no_of_busy_days != no_of_days:
extend_future_date_by = no_of_days - no_of_busy_days
future_date = future_date + timedelta(extend_future_date_by)
return future_date
This is the best solution because it has O(1) complexity (no loop) and no 3-rd party, but it does not take into account the holidays:
def add_working_days_to_date(self, start_date, days_to_add):
from datetime import timedelta
start_weekday = start_date.weekday()
# first week
total_days = start_weekday + days_to_add
if total_days < 5:
return start_date + timedelta(days=total_days)
else:
# first week
total_days = 7 - start_weekday
days_to_add -= 5 - start_weekday
# middle whole weeks
whole_weeks = days_to_add // 5
remaining_days = days_to_add % 5
total_days += whole_weeks * 7
days_to_add -= whole_weeks * 5
# last week
total_days += remaining_days
return start_date + timedelta(days=total_days)
Even though this does not fully solves your problem, I wanted to let it here because the solutions found on the internet for adding working days to dates, all of them have O(n) complexity.
Keep in mind that, if you want to add 500 days to a date, you will go through a loop and make the same set of computations 500 times. The above approach operates in the same amount of time, no matter how many days you have.
This was heavily tested.
Use numpy (you can skip holidays too):
np.busday_offset(
np.datetime64('2022-12-08'),
offsets=range(12),
roll='following',
weekmask="1111100",
holidays=[])
Result:
array(['2022-12-08', '2022-12-09', '2022-12-12', '2022-12-13',
'2022-12-14', '2022-12-15', '2022-12-16', '2022-12-19',
'2022-12-20', '2022-12-21', '2022-12-22', '2022-12-23'],
dtype='datetime64[D]')
I am using following code to handle business date delta. For holidays, you need to create your own list to skip.
today = datetime.now()
t_1 = today - BDay(1)
t_5 = today - BDay(5)
t_1_str = datetime.strftime(t_1,"%Y%m%d")

Categories