Efficiently Finding the next Friday The 13th - python

I have been trying to calculate the date for next Friday the 13th in Python 3, but Idk how to make it more efficient.
from datetime import date, timedelta
from calendar import monthrange
def is_friday_13th(date):
# Returns bool value for the given date if it is Friday The 13th
return date.day == 13 and date.weekday() == 4
def max_days(date):
# Returns Number of Days for the given month
return monthrange(date.year, date.month)[1]
def friday_the_13th():
# Returns next Friday the 13th
# If today is Friday the 13th, returns today's date.
today = date.today()
result = today
if result.day < 13:
result += timedelta(days=13-result.day)
found = is_friday_13th(result)
while not found:
result += timedelta(days=max_days(result) - result.day)
result += timedelta(days=13)
found = is_friday_13th(result)
return f"{result.strftime('%Y-%m-%d')}"
if __name__ == "__main__":
print(friday_the_13th())
I feel like using the monthrange function of the calendar module makes it less efficient and this problem can be solved without it but I'm struggling to do it.
I read the solutions written by others and I don't understand how to solve this problem more efficiently and write more Pythonically.

How about this
from datetime import date, timedelta
def friday13s(from_date=date.today()):
d = from_date + timedelta(13 - from_date.day) # clamp date to the 13th
def increment_month(d):
mm = 1 if d.month == 12 else d.month + 1
yy = d.year + 1 if mm == 1 else d.year
return date(yy, mm, d.day)
if from_date > d:
d = increment_month(d)
while True:
if d.weekday() == 4:
yield d
d = increment_month(d)
usage:
from itertools import islice
for d in islice(friday13s(), 10):
print(d)
prints
2021-08-13
2022-05-13
2023-01-13
2023-10-13
2024-09-13
2024-12-13
2025-06-13
2026-02-13
2026-03-13
2026-11-13
Note that increment_month() is not a general-purpose way to increment the month in a date. It works fine if the day is the 13th, but it will fail if the day is >= 29th. It's only fit for the purpose of the particular task it is used in here.
There are more elegant ways of incrementing the month with libraries like dateutil, but when using only built-ins from the Python standard library, doing some legwork is necessary. Luckily, the date arithmetic for this task is not complicated. Adding a whole extra library just to reduce the above by two or three lines seems excessive.

Related

Dates/months calculation

My below working code calculates date/month ranges, but I am using the Pandas library, which I want to get rid of.
import pandas as pd
dates=pd.date_range("2019-12","2020-02",freq='MS').strftime("%Y%m%d").tolist()
#print dates : ['20191101','20191201','20200101','20200201']
df=(pd.to_datetime(dates,format="%Y%m%d") + MonthEnd(1)).strftime("%Y%m%d").tolist()
#print df : ['20191130','20191231','20200131','20200229']
How can I rewrite this code without using Pandas?
I don't want to use Pandas library as I am triggering my job through Oozie and we don't have Pandas installed on all our nodes.
Pandas offers some nice functionalities when using datetimes which the standard library datetime module does not have (like the frequency or the MonthEnd). You have to reproduce these yourself.
import datetime as DT
def next_first_of_the_month(dt):
"""return a new datetime where the month has been increased by 1 and
the day is always the first
"""
new_month = dt.month + 1
if new_month == 13:
new_year = dt.year + 1
new_month = 1
else:
new_year = dt.year
return DT.datetime(new_year, new_month, day=1)
start, stop = [DT.datetime.strptime(dd, "%Y-%m") for dd in ("2019-11", "2020-02")]
dates = [start]
cd = next_first_of_the_month(start)
while cd <= stop:
dates.append(cd)
cd = next_first_of_the_month(cd)
str_dates = [d.strftime("%Y%m%d") for d in dates]
print(str_dates)
# prints: ['20191101', '20191201', '20200101', '20200201']
end_dates = [next_first_of_the_month(d) - DT.timedelta(days=1) for d in dates]
str_end_dates = [d.strftime("%Y%m%d") for d in end_dates]
print(str_end_dates)
# prints ['20191130', '20191231', '20200131', '20200229']
I used here a function to get a datetime corresponding to the first day of the next month of the input datetime. Sadly, timedelta does not work with months, and adding 30 days of course is not feasible (not all months have 30 days).
Then a while loop to get a sequence of fist days of the month until the stop date.
And to the get the end of the month, again get the next first day of the month fo each datetime in your list and subtract a day.

python parse date string with out of range days without error

I want to parse a date string and manipulate the year, month, date in cases where I either get '00' for month or day or in cases where I get a day beyond the possible days of that year/month. Given a '2012-00-00' or a '2020-02-31', I get a ValueError. What I want, is to catch the error and then turn the former into '2012-01-01' and the latter to '2020-02-29'. No results on Google so far.
Clarification: I use try/except/ValueError... what I want is to parse out the year, month, day and fix the day or month when they are having a ValueError... without having to code the parsing and regular expressions myself... which defeats the purpose of using a library to begin with.
# Try dateutjil
blah = dateutil.parser.parse(date_string, fuzzy=True)
print(blah)
# Try datetime
date_object = datetime.strptime(date_string, date_format)
return_date_string = date_object.date().strftime('%Y-%m-%d')
I know you don't want to parse the date yourself but I think you will probably have to. One option would be to split the incoming string into its component year, month and day parts and check them against valid values, adjusting as required. You can then create a date from that and call strftime to get a valid date string:
from datetime import datetime, date
import calendar
def parse_date(dt):
[y, m, d] = map(int, dt.split('-'))
# optional error checking on y
# ...
# check month
m = 1 if m == 0 else 12 if m > 12 else m
# check day
last = calendar.monthrange(y, m)[-1]
d = 1 if d == 0 else last if d > last else d
return date(y, m, d).strftime('%Y-%m-%d')
print(parse_date('2012-00-00'))
print(parse_date('2020-02-31'))
Output:
2012-01-01
2020-02-29

Python get date from weekday

Let's say I have 11 Sessions for myself to complete. I haven't set dates for these sessions but rather just weekdays where one session would take place. Let's say when scheduling these sessions, I chose MON, TUE and WED. This means that after today, I want the dates to 11 my sessions which would be 4 Mondays, 4 Tuesdays and 3 Wednesdays from now after which my sessions will be completed.
I want to automatically get the dates for these days until there are 11 dates in total.
I really hope this makes sense... Please help me. I've been scratching my head over this for 3 hours straight.
Thanks,
You can use pd.date_range and the CustomBusinessDay object to do this very easily.
You can use the CustomBusinessDay to specify your "business days" and create your date range from it:
import pandas
from datetime import date
session_days = pd.offset.CustomBusinessDay(weekmask="Mon Tue Wed")
dates = pd.date_range(date.today(), freq=session_days, periods=11)
I figured it out a while ago but my internet died. All it took was Dunhill and some rest.
import datetime
def get_dates():
#This is the max number of dates you want. In my case, sessions.
required_sessions = 11
#These are the weekdays you want these sessions to be
days = [1,2,3]
#An empty list to store the dates you get
dates = []
#Initialize a variable for the while loop
current_sessions = 0
#I will start counting from today but you can choose any date
now = datetime.datetime.now()
#For my use case, I don't want a session on the same day I run this function.
#I will start counting from the next day
if now.weekday() in days:
now = now + datetime.timedelta(days=1)
while current_sessions != required_sessions:
#Iterate over every day in your desired days
for day in days:
#Just a precautionary measure so the for loops breaks as soon as you have the max number of dates
#Or the while loop will run for ever
if current_sessions == required_sessions:
break
#If it's Saturday, you wanna hop onto the next week
if now.weekday() == 6:
#Check if Sunday is in the days, add it
if 0 in days:
date = now + datetime.timedelta(days=1)
dates.append(date)
current_sessions += 1
now = date
else:
#Explains itself.
if now.weekday() == day:
dates.append(now)
now = now + datetime.timedelta(days=1)
current_sessions += 1
#If the weekday today is greater than the day you're iterating over, this means you've iterated over all the days in a NUMERIC ORDER
#NOTE: This only works if the days in your "days" list are in a correct numeric order meaning 0 - 6. If it's random, you'll have trouble
elif not now.weekday() > day:
difference = day - now.weekday()
date = now + datetime.timedelta(days=difference)
dates.append(date)
now = date
current_sessions += 1
#Reset the cycle after the for loop is done so you can hop on to the next week.
reset_cycle_days = 6 - now.weekday()
if reset_cycle_days == 0:
original_now = now + datetime.timedelta(days=1)
now = original_now
else:
original_now = now + datetime.timedelta(days=reset_cycle_days)
now = original_now
for date in dates:(
print(date.strftime("%d/%m/%y"), date.weekday()))
Btw, I know this answer is pointless compared to #Daniel Geffen 's answer. If I were you, I would definitely choose his answer as it is very simple. This was just my contribution to my own question in case anyone would want to jump into the "technicalities" of how it's done by just using datetime. For me, this works best as I'm having issues with _bz2 in Python3.7 .
Thank you all for your help.

Generate list of tuples (year, month, days_in_month, full_month) from list of dates

I have a list of dates as generated by:
from dateutil import parser
from datetime import date, timedelta
d1 = parser.parse("2015-11-25")
d2 = parser.parse("2016-02-06")
delta = (d2-d1).days
date_list = [d1 + timedelta(days=x) for x in range(0, delta+1)]
In this list there are 6 days in the month of november 2015, 31 days in december 2015 , 31 days in january 2016 and 6 days in february 2016. December 2015 and January 2016 are "full" months, i.e. the datelist has all days in those months.
How can I get this information programatically in python, in order to produce a list such as:
[(2015,11,6,False),(2015,12,31,True),(2016,1,31,True),(2016,2,6,False)]
Found a neat short solution:
from dateutil import parser
from datetime import date, timedelta
from collections import Counter
from calendar import monthrange
d1 = parser.parse("2015-11-25")
d2 = parser.parse("2016-02-06")
delta = (d2-d1).days
date_list = [d1 + timedelta(days=x) for x in range(0, delta+1)]
month_year_list = [(d.year, d.month) for d in date_list]
result = [(k[0],k[1],v , True if monthrange(k[0], k[1])[1] == v else
False) for k,v in Counter(month_year_list).iteritems()]
print result
Walk the list and accumulate the number of days for each year/month combination:
import collections
days_in_year_month = defaultdict(int)
for each_date in date_list:
days_in_year_month[(each_date.year, each_date.month)] += 1
Next output the tuples with each year, month, count and T/F:
import calendar
result = []
for year_month in date_list.keys():
days_in_ym = days_in_year_month([year_month[0], year_month[1])
is_complete = days_in_ym == calendar.monthrange(year_month[0], year_month[1])[1]
result.append(year_month[0], year_month[1], days_in_ym, is_complete)
So:
I learned about monthrange here: How do we determine the number of days for a given month in python
My solution sucks because it will do a total of 3 loops: the initial loop from your list comprehension, plus the two loops I added. Since you're walking the days in order for your list comprehension, this could be much optimized to run in a single loop.
I didn't test it :)
The previous mentioned solutions seem ok, however I believe I have a more optimal solution, since they require to calculate a list that contains all the days. For a small date difference this won't be problematic. However if the difference increases, your list will become a lot larger.
I want to give another approach that is more intuitive, since you basically know that all months that between the dates are full, and the months of the dates themselves are not full.
I try to leverage that information and the loop will only iterate the amount of months between the dates.
The code:
from dateutil import parser
from calendar import monthrange
d1 = parser.parse("2015-11-25")
d2 = parser.parse("2016-02-06")
# needed to calculate amount of months between the dates
m1 = d1.year * 12 + (d1.month- 1)
m2 = d2.year * 12 + (d2.month - 1)
result = []
# append first month since this will not be full
result.append((d1.year,d1.month,monthrange(d1.year, d1.month)[1]-d1.day+1,False))
current_month = d1.month
current_year = d1.year
# loop through the months and years that follow d1.
for _ in xrange(0,(m2-m1)-1):
if current_month+1 > 12:
current_month = 1
current_year += 1
else:
current_month += 1
result.append((current_year,current_month,monthrange(current_year, current_month)[1],True))
# append last month since this will not be full either.
result.append((d2.year,d2.month,d2.day,False))
print result
Keep in mind that the code I gave is an example, it doesn't support for instance the scenario where the 2 given dates have the same month.

nth weekday calculation in Python - whats wrong with this code?

I'm trying to calculate the nth weekday for a given date. For example, I should be able to calculate the 3rd wednesday in the month for a given date.
I have written 2 versions of a function that is supposed to do that:
from datetime import datetime, timedelta
### version 1
def nth_weekday(the_date, nth_week, week_day):
temp = the_date.replace(day=1)
adj = (nth_week-1)*7 + temp.weekday()-week_day
return temp + timedelta(days=adj)
### version 2
def nth_weekday(the_date, nth_week, week_day):
temp = the_date.replace(day=1)
adj = temp.weekday()-week_day
temp += timedelta(days=adj)
temp += timedelta(weeks=nth_week)
return temp
Console output
# Calculate the 3rd Friday for the date 2011-08-09
x=nth_weekday(datetime(year=2011,month=8,day=9),3,4)
print 'output:',x.strftime('%d%b%y')
# output: 11Aug11 (Expected: '19Aug11')
The logic in both functions is obviously wrong, but I can't seem to locate the bug - can anyone spot what is wrong with the code - and how do I fix it to return the correct value?
Your problem is here:
adj = temp.weekday()-week_day
First of all, you are subtracting things the wrong way: you need to subtract the actual day from the desired one, not the other way around.
Second, you need to ensure that the result of the subtraction is not negative - it should be put in the range 0-6 using % 7.
The result:
adj = (week_day - temp.weekday()) % 7
In addition, in your second version, you need to add nth_week-1 weeks like you do in your first version.
Complete example:
def nth_weekday(the_date, nth_week, week_day):
temp = the_date.replace(day=1)
adj = (week_day - temp.weekday()) % 7
temp += timedelta(days=adj)
temp += timedelta(weeks=nth_week-1)
return temp
>>> nth_weekday(datetime(2011,8,9), 3, 4)
datetime.datetime(2011, 8, 19, 0, 0)
one-liner
You can find the nth weekday with a one liner that uses calendar from the standard library.
import calendar
calendar.Calendar(x).monthdatescalendar(year, month)[n][0]
where:
x : the integer representing your weekday (0 is Monday)
n : the 'nth' part of your question
year, month : the integers year and month
This will return a datetime.date object.
broken down
It can be broken down this way:
calendar.Calendar(x)
creates a calendar object with weekdays starting on your required weekday.
.monthdatescalendar(year, month)
returns all the calendar days of that month.
[n][0]
returns the 0 indexed value of the nth week (the first day of that week, which starts on the xth day).
why it works
The reason for starting the week on your required weekday is that by default 0 (Monday) will be used as the first day of the week and if the month starts on a Wednesday, calendar will consider the first week to start on the first occurrence of Monday (ie. week 2) and you'll be a week behind.
example
If you were to need the third Saturday of September 2013 (that month's US stock option expiry day), you would use the following:
calendar.Calendar(5).monthdatescalendar(2013,9)[3][0]
The problem with the one-liner with the most votes is it doesn't work.
It can however be used as a basis for refinement:
You see this is what you get:
c = calendar.Calendar(calendar.SUNDAY).monthdatescalendar(2018, 7)
for c2 in c:
print(c2[0])
2018-07-01
2018-07-08
2018-07-15
2018-07-22
2018-07-29
c = calendar.Calendar(calendar.SUNDAY).monthdatescalendar(2018, 8)
for c2 in c:
print(c2[0])
2018-07-29
2018-08-05
2018-08-12
2018-08-19
2018-08-26
If you think about it it's trying to organise the calendars into nested lists to print a weeks worth of dates at a time. So stragglers from other months come into play. By using a new list of valid days that fall in the month - this does the trick.
Answer with appended list
import calendar
import datetime
def get_nth_DOW_for_YY_MM(dow, yy, mm, nth) -> datetime.date:
#dow - Python Cal - 6 Sun 0 Mon ... 5 Sat
#nth is 1 based... -1. is ok for last.
i = -1 if nth == -1 or nth == 5 else nth -1
valid_days = []
for d in calendar.Calendar(dow).monthdatescalendar(yy, mm):
if d[0].month == mm:
valid_days.append(d[0])
return valid_days[i]
So here's how it could be called:
firstSundayInJuly2018 = get_nth_DOW_for_YY_MM(calendar.SUNDAY, 2018, 7, 1)
firstSundayInAugust2018 = get_nth_DOW_for_YY_MM(calendar.SUNDAY, 2018, 8, 1)
print(firstSundayInJuly2018)
print(firstSundayInAugust2018)
And here is the output:
2018-07-01
2018-08-05
get_nth_DOW_for_YY_MM() can be refactored using lambda expressions like so:
Answer with lambda expression refactoring
import calendar
import datetime
def get_nth_DOW_for_YY_MM(dow, yy, mm, nth) -> datetime.date:
#dow - Python Cal - 6 Sun 0 Mon ... 5 Sat
#nth is 1 based... -1. is ok for last.
i = -1 if nth == -1 or nth == 5 else nth -1
return list(filter(lambda x: x.month == mm, \
list(map(lambda x: x[0], \
calendar.Calendar(dow).monthdatescalendar(yy, mm) \
)) \
))[i]
The one-liner answer does not seem to work if the target day falls on the first of the month. For instance, if you want the 2nd Friday of every month, then the one-liner approach
calendar.Calendar(4).monthdatescalendar(year, month)[2][0]
for March 2013 will return March 15th 2013 when it should be March 8th 2013. Perhaps add in a check like
if date(year, month, 1).weekday() == x:
delivery_date.append(calendar.Calendar(x).monthdatescalendar(year, month)[n-1][0])
else:
delivery_date.append(calendar.Calendar(x).monthdatescalendar(year, month)[n][0])
Alternatively this will work for Python 2, returns the occurance of weekday in the said month, i.e if 16 June 2018 is the input, then returns the occurance of the day on 16th June 2018
You may substitute the month/year/date integers to anything you might want - right now it's getting the input / date from the system via datetime
Omit out print statements or use pass where they're not needed
import calendar
import datetime
import pprint
month_number = int(datetime.datetime.now().strftime('%m'))
year_number = int(datetime.datetime.now().strftime('%Y'))
date_number = int(datetime.datetime.now().strftime('%d'))
day_ofweek = str(datetime.datetime.now().strftime('%A'))
def weekday_occurance():
print "\nFinding current date here\n"
for week in xrange(5):
try:
calendar.monthcalendar(year_number, month_number)[week].index(date_number)
occurance = week + 1
print "Date %s of month %s and year %s is %s #%s in this month." % (date_number,month_number,year_number,day_ofweek,occurance)
return occurance
break
except ValueError as e:
print "The date specified is %s which is week %s" % (e,week)
myocc = weekday_occurance()
print myocc
A little tweak would make the one-liner work correctly:
import calendar
calendar.Calendar((weekday+1)%7).monthdatescalendar(year, month)[n_th][-1]
Here n_th should be interpreted as c-style, e.g. 0 is the first index.
Example: to find 1st Sunday in July 2018 one could type:
>>> calendar.Calendar(0).monthdatescalendar(2018, 7)[0][-1]
datetime.date(2018, 7, 1)
People here seem to like one-liner, I will propose below.
import calendar
[cal[0] for cal in calendar.Calendar(x).monthdatescalendar(year, month) if cal[0].month == month][n]
The relativedelta module that's an extension from the Python dateutil package (pip install python-dateutil) does exactly what you want:
from dateutil import relativedelta
import datetime
def nth_weekday(the_date, nth_week, week_day):
return the_date.replace(day=1) + relativedelta.relativedelta(
weekday=week_day(nth_week)
)
print(nth_weekday(datetime.date.today(), 3, relativedelta.FR))
The key part here evaluates to weekday=relativedelta.FR(3): 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)).

Categories