Calculate the next and the 3rd business date from a given date - python

I am trying to implement a function to calculate the next and 3rd business day from a given date (ideally taking into account some given holidays)
def day_of_week(year, month, day):
t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]
year -= month < 3
return (year + int(year/4) - int(year/100) + int(year/400) + t[month-1] + day) % 7
The input is in the format YYYYMMDD with 21th of March 2018 written as 20180321 and the output date should be in the same format.
I`m trying to do something like this but I realised this is not the best practice
def leap_year(year):
if(year%4==0 and year%100!=0):
return True
elif (year%4==0 and year%100==0 and year%400==0):
return True
else:
return False
def business_day(year, month, day):
if (month==12):
if(day_of_week(year, month, day)<5 and day_of_week(year, month, day)>0 and day==31):
return str(year+1)+str(0)+str(1)+str(0)+str(1)
elif (day_of_week(year, month, day)<5 and day_of_week(year, month, day)>0 and day!=31):
newDay="0"
if(day<10):
newDay = newDay + str(day+1)
else:
newDay = str(day+1)
return str(year) + str(month) + newDay
elif (day_of_week(year, month, day)>=5 and day==31):
if(day_of_week(year, month, day)==5):
return str(year+1)+"01"+"03"
if (day_of_week(year, month, day) == 6):
return str(year + 1) + "01" + "02"
if (day_of_week(year, month, day) == 0):
return str(year + 1) + "01" + "01"
elif (day_of_week(year, month, day)>=5 and day==30):
if((day_of_week(year, month, day)==5)):
return str(year + 1) + "01" + "02"
if ((day_of_week(year, month, day) == 6)):
return str(year + 1) + "01" + "01"
if ((day_of_week(year, month, day) == 0)):
return str(year + 1) + str(month) + str(day+1)
Can`t use any libraries in the solution. Thanks for the help

No libraries! I had some fun learning Python. Did you? :-)
def day_of_week(year, month, day):
t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]
year -= month < 3
dw = (year + year // 4 - year // 100 + year // 400 + t[month-1] + day) % 7
return [6, 0, 1, 2, 3, 4, 5][dw] # To be consistent with 'datetime' library
def leap_year(year):
leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
return True if leap else False
def valid_day(year, month, day):
month_list = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if year < 1 or year > 9999 or month < 1 or month > 12:
return False
m = month_list[month-1] if month != 2 or not leap_year(year) else 29
return True if 1 <= day <= m else False
class Career(Exception):
def __str__(self): return 'So I became a waiter...'
MAX_DATE_AND_DAYS_INT = 365 * 100
class Date:
# raise ValueError
def __init__(self, year, month, day):
if not valid_day(year, month, day):
raise Career()
self.y, self.m, self.d = year, month, day
#classmethod
def fromstring(cls, s):
s1, s2, s3 = int(s[0:4]), int(s[4:6]), int(s[6:8])
return cls(s1, s2, s3)
def __repr__(self) -> str:
return '%04d%02d%02d' % (self.y, self.m, self.d)
def weekday_date(self) -> str:
names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
return names[self.weekday()] + ' ' + str(self)
def next_day(self):
if valid_day(self.y, self.m, self.d + 1):
return Date(self.y, self.m, self.d + 1)
elif valid_day(self.y, self.m + 1, 1):
return Date(self.y, self.m + 1, 1)
elif valid_day(self.y + 1, 1, 1):
return Date(self.y + 1, 1, 1)
else:
raise Career
def weekday(self):
return day_of_week(self.y, self.m, self.d)
def __add__(self, other):
"Add a Date to an int."
if isinstance(other, int):
if other < 1 or other > MAX_DATE_AND_DAYS_INT:
raise OverflowError("int > MAX_DATE_AND_DAYS_INT")
new_date = Date(self.y, self.m, self.d)
while other >= 1:
new_date = new_date.next_day()
other -= 1
return new_date
return NotImplemented
def next_working_day(self):
day = self.next_day()
while True:
while day.weekday() >= 5:
day = day.next_day()
holidays_list = year_holidays(day.y)
for str_day in holidays_list:
s2 = str(day)
if str_day == s2:
day = day.next_day()
break # for
if day.weekday() < 5:
break # while True
return day
def year_holidays(year):
holidays = [
["New Year's Day", 1, 1], # Fixed: January 1
["Birthday of Martin Luther King, Jr.", 1, 0, 0, 3], # Floating
["Washington's Birthday", 2, 0, 0, 3], # Third Monday in February
["Memorial Day", 5, 0, 0, 5], # Last Monday
["Independence Day", 7, 4],
["Labor Day", 9, 0, 0, 1],
["Columbus Day", 10, 0, 0, 2],
["Veterans Day", 11, 11],
["Thanksgiving Day", 11, 0, 3, 4],
["Christmas Day", 12, 25]
]
year_list = []
for h in holidays:
if h[2] > 0:
day = Date(year, h[1], h[2]) # Fixed day
else:
day = Date(year, h[1], 1) # Floating day
while h[3] != day.weekday(): # Advance to match the weekday
day = day.next_day()
count = 1
while count != h[4]: # Match the repetition of this day
next_week = day + 7
if next_week.m == day.m:
day = next_week
count += 1
year_list.append(str(day))
return year_list # return the holidays as list of strings
if __name__ == '__main__':
dates = [
['20190308', '20190311', '20190313'],
['20190309', '20190311', '20190313'],
['20190310', '20190311', '20190313'],
['20190311', '20190312', '20190314'],
['20190329', '20190401', '20190403'],
['20181231', '20190102', '20190104'],
['20190118', '20190122', '20190124'],
['20190216', '20190219', '20190221'],
['20190526', '20190528', '20190530'],
['20190703', '20190705', '20190709'],
['20190828', '20190829', '20190903'],
['20191010', '20191011', '20191016'],
['20191108', '20191112', '20191114'],
['20191125', '20191126', '20191129'],
['20191224', '20191226', '20191230'],
['20191227', '20191230', '20200102']]
print('\n Today Next and 3rd business day')
for days in dates:
today = Date.fromstring(days[0])
next_day = today.next_working_day()
third_day = next_day.next_working_day().next_working_day()
if str(next_day) != days[1] or str(third_day) != days[2]:
print('*** ERROR *** ', end='')
else:
print(' ', end='')
print(today.weekday_date(), next_day.weekday_date(), third_day.weekday_date())
Output:
Today Next and 3rd business day
Fri 20190308 Mon 20190311 Wed 20190313
Sat 20190309 Mon 20190311 Wed 20190313
Sun 20190310 Mon 20190311 Wed 20190313
Mon 20190311 Tue 20190312 Thu 20190314
Fri 20190329 Mon 20190401 Wed 20190403
Mon 20181231 Wed 20190102 Fri 20190104
Fri 20190118 Tue 20190122 Thu 20190124
Sat 20190216 Tue 20190219 Thu 20190221
Sun 20190526 Tue 20190528 Thu 20190530
Wed 20190703 Fri 20190705 Tue 20190709
Wed 20190828 Thu 20190829 Tue 20190903
Thu 20191010 Fri 20191011 Wed 20191016
Fri 20191108 Tue 20191112 Thu 20191114
Mon 20191125 Tue 20191126 Fri 20191129
Tue 20191224 Thu 20191226 Mon 20191230
Fri 20191227 Mon 20191230 Thu 20200102

import datetime
example = '20180321'
# you can parse the time string directly to a datetime object
next_buisness_day = datetime.datetime.strptime(example, '%Y%m%d')
# specify the increment based on the day of the week or any
#other condition
increment = 1
print('day day is', next_buisness_day.weekday())
# if friday
if next_buisness_day.weekday() == 4:
increment = 3
# if saturday
elif next_buisness_day.weekday() == 5:
increment = 2
next_buisness_day += datetime.timedelta(days=increment)
# and convert back to whatever format you like
print('{:%Y%m%d}'.format(next_buisness_day))
Have a look at the datetime module you can do all sorts of things with it.
https://docs.python.org/3/library/datetime.html

I use few functions from 'datetime' library. You can have fun to write them: date(y, m, d), timedelta(days=7), day,weekday(), '{:%Y%m%d}'.format(day), strptime(input, '%Y%m%d'), strftime(datetime, '%a %x'). Good idea is to create a class for date and get rid from all format conversions. So, only date(y, m, d), timedelta(days=7), day, weekday() will be left for exercise.
import datetime
from datetime import date, timedelta
def day2string(day):
return '{:%Y%m%d}'.format(day)
def year_holidays(year):
holidays = [
["New Year's Day", 1, 1], # Fixed: January 1
["Birthday of Martin Luther King, Jr.", 1, 0, 0, 3], # Floating
["Washington's Birthday", 2, 0, 0, 3], # Third Monday in February
["Memorial Day", 5, 0, 0, 5], # Last Monday
["Independence Day", 7, 4],
["Labor Day", 9, 0, 0, 1],
["Columbus Day", 10, 0, 0, 2],
["Veterans Day", 11, 11],
["Thanksgiving Day", 11, 0, 3, 4],
["Christmas Day", 12, 25]
]
year_list = []
for h in holidays:
if h[2] > 0:
day = date(year, h[1], h[2]) # Fixed day
else:
day = date(year, h[1], 1) # Floating day
while h[3] != day.weekday(): # Advance to match the weekday
day += timedelta(days=1)
count = 1
while count != h[4]: # Match the repetition of this day
next_week = day + timedelta(days=7)
if next_week.month == day.month:
day = next_week
count += 1
year_list.append(day2string(day))
return year_list # return the holidays as list of strings
def str2datetime(string):
return datetime.datetime.strptime(string, '%Y%m%d')
def next_working_day(string):
day = str2datetime(string)
day += timedelta(days=1)
while True:
while day.weekday() >= 5:
day += timedelta(days=1)
holidays_list = year_holidays(day.year)
for str_day in holidays_list:
s2 = day2string(day)
if str_day == s2:
day += timedelta(days=1)
break # for
if day.weekday() < 5:
break # while True
return day2string(day)
if __name__ == '__main__':
dates = [
['20190308', '20190311', '20190313'],
['20190309', '20190311', '20190313'],
['20190310', '20190311', '20190313'],
['20190311', '20190312', '20190314'],
['20190329', '20190401', '20190403'],
['20181231', '20190102', '20190104'],
['20190118', '20190122', '20190124'],
['20190216', '20190219', '20190221'],
['20190526', '20190528', '20190530'],
['20190703', '20190705', '20190709'],
['20190828', '20190829', '20190903'],
['20191010', '20191011', '20191016'],
['20191108', '20191112', '20191114'],
['20191125', '20191126', '20191129'],
['20191224', '20191226', '20191230'],
['20191227', '20191230', '20200102']]
print('\n Today Next and 3rd business day')
for days in dates:
next_day = next_working_day(days[0])
third_day = next_working_day(next_working_day(next_day))
if next_day != days[1] or third_day != days[2]:
print('*** ERROR *** ', end='')
else:
print(' ', end='')
def f(x): return datetime.datetime.strftime(str2datetime(x), '%a %x')
print(f(days[0]), f(next_day), f(third_day))
It should create the next output:
Today Next and 3rd business day
Fri 03/08/19 Mon 03/11/19 Wed 03/13/19
Sat 03/09/19 Mon 03/11/19 Wed 03/13/19
Sun 03/10/19 Mon 03/11/19 Wed 03/13/19
Mon 03/11/19 Tue 03/12/19 Thu 03/14/19
Fri 03/29/19 Mon 04/01/19 Wed 04/03/19
Mon 12/31/18 Wed 01/02/19 Fri 01/04/19
Fri 01/18/19 Tue 01/22/19 Thu 01/24/19
Sat 02/16/19 Tue 02/19/19 Thu 02/21/19
Sun 05/26/19 Tue 05/28/19 Thu 05/30/19
Wed 07/03/19 Fri 07/05/19 Tue 07/09/19
Wed 08/28/19 Thu 08/29/19 Tue 09/03/19
Thu 10/10/19 Fri 10/11/19 Wed 10/16/19
Fri 11/08/19 Tue 11/12/19 Thu 11/14/19
Mon 11/25/19 Tue 11/26/19 Fri 11/29/19
Tue 12/24/19 Thu 12/26/19 Mon 12/30/19
Fri 12/27/19 Mon 12/30/19 Thu 01/02/20

Related

Python - Basic Leap year function problem (Novice)

So basically my problem is, I have a list from 2020 to 2030 and my program said every year is a leap year.
My variables are:
yearList = [2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030]
monthList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
daysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
def create_calendar(yearList: list, monthList: list, daysOfMonth: list, numbOfShootingStars: int):
calendar = {}
for year in yearList:
# Create year
calendar[year] = {}
for month in monthList:
# If the list don't have 12 months, it won't loop through all months
if month == 12 and month + 1 == 1: break;
else:
# Create monthly data
calendar[year][month] = {}
# If February is a leap year, it will have 29 days instead of 28
if month == 2 and year % 4 == 0:
daysOfMonth[month - 1] = 29
# Create the days and daily data
for day in range(1, daysOfMonth[monthList.index(month)] + 1):
calendar[year][month][day] = numbOfShootingStars
return calendar
Thank you for your help!
1 question, is it possible to use a list like this for this method?
monthList = [
{1, 'January'},
{2, 'February'},
{3, 'March'},
{4, 'April'},
{5, 'May'},
{6, 'June'},
{7, 'July'},
{8, 'August'},
{9, 'September'},
{10, 'October'},
{11, 'November'},
{12, 'December'}]
Then how should I modify my code because I couldn't get it work :(
Okay, I think I solved it, the problem was, I changed to value in the daysOfMonth list at February to 29 and then it stays like that. With this if - else I changed back to it's original state when it is not a leap year.
# If February is a leap year, it will have 29 days instead of 28
if month == 2 and year % 4 == 0:
daysOfMonth[month - 1] = 29
else:
daysOfMonth[month - 1] = 28
If you take a year to be 365.2425 days, this is 365 + 1/4 - 1/100 + 1/400. This explains why in the long run you stick to the correct value by adding a whole day every fourth year, but not on every century, but on every fourth century (starting from 0).
A possible implementation is, to tell if the year Y is leap:
Floor(365.2425 Y) - Floor(365.2425 (Y-1)) - 365

How can I change 24 hour time format to 12 hour format using if and else?

Example if we type 0010 then it should display 12:10 AM like that for every number it should display.,
military_times = ['0010', '0125', '1159', '1200', '1201',
'1259', '1344', '1959', '2359']
for item in military_times:
# time is between 0000 and 0059
if int(item[0: 2]) == 0:
t = '12:' + item[2: 4] + ' AM'
# time is between 1:00 am and 9:59 am
elif int(item[0: 2]) > 0 and int(item[0: 2]) <= 9:
t = str(item[1: 2]) + ':' + item[2: 4] + ' AM'
# time is between 10:00 am and 11:59 am
elif int(item[0: 2]) > 0 and int(item[0: 2]) <= 11:
t = str(item[0: 2]) + ':' + item[2: 4] + ' AM'
# time is between 12:00 pm and 12:59 pm
elif int(item[0: 2]) > 0 and int(item[0: 2]) == 12:
t = str(item[0: 2]) + ':' + item[2: 4] + ' PM'
# time is between 12:00 pm and 11:59 pm
else:
t = str(int(item[0: 2]) - 12) + ':' + item[2: 4] + ' PM'
print t
'''
# output
12:10 AM
1:25 AM
11:59 AM
12:00 PM
12:01 PM
12:59 PM
1:44 PM
7:59 PM
11:59 PM
'''
def to_12_hour(ts):
hr = int(ts[:2])
mins = int(ts[-2:])
return "{0}:{1}{2}".format(hr, mins, 'AM' if hr < 12 else 'PM')

Days old udacity

I have a problem in these two casesprint daysBetweenDates(2011, 1, 1, 2012, 8, 8)
print daysBetweenDates(1900,1,1, 1999,12, 31)when I put them with the other test cases I got a wrong answer by 1 day extra and sometimes by 2 days.sometimes one of them give me the right answer but it also appears asTest with data:(2011, 1, 1, 2012,8,8)failed
Test with data: (1900, 1, 1, 1999, 12, 31) failed but when I test each case alone i got the right answer.
daysofmonths = [ 0,31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def leap_year(year):
leap_day = 366
common_day = 365
if year % 4 != 0:
return common_day
elif year % 100 != 0:
return leap_day
elif year % 400 !=0:
return common_day
else:
return leap_day
def daysBetweenDates(year1, month1, day1, year2, month2, day2):
#code for same year
if year1 == year2:
if month1 == month2:
return day2 - day1
days = daysofmonths[month1] - day1
month1 = month1 + 1
while month1 < month2:
if leap_year(year1) == 366:
daysofmonths[2] = 29
days = days + daysofmonths[month1]
month1 = month1 + 1
return days + day2
################################################
days = daysofmonths[month1] - day1
month1 = month1 + 1
while month1 <= 12:
if leap_year(year1) == 366:
daysofmonths[2] = 29
days = days + daysofmonths[month1]
month1 = month1 + 1
#print days
year1 = year1 + 1
###########################################################
days = days + day2
month2 = month2 - 1
while month2 >= 1:
if leap_year(year2) == 366:
daysofmonths[2] = 29
days = days + daysofmonths[month2]
month2 = month2 - 1
#print days
year2 = year2 - 1
###########################################################
while year1 <= year2:
days = days + leap_year(year1)
year1 = year1 + 1
return days
print daysBetweenDates(2011, 1, 1, 2012, 8, 8)
print daysBetweenDates(1900,1,1, 1999,12, 31)
def test():
test_cases = [((2012,1,1,2012,2,28), 58),
((2012,1,1,2012,3,1), 60),
((2011,6,30,2012,6,30), 366),
((2011,1,1,2012,8,8), 585 ),
((1900,1,1,1999,12,31), 36523)]
for (args, answer) in test_cases:
result = daysBetweenDates(*args)
if result != answer:
print "Test with data:", args, "failed"
else:
print "Test case passed!"
test()
when you do:
daysofmonths[2] = 29
it changes the element in the list, which then is used for every subsequent call, if you added print(daysofmonths[2]) in between the test cases you would see that it is always 29 after the first case that needs to check February, so instead of conditionally changing the list with:
if leap_year(year1) == 366:
daysofmonths[2] = 29
days = days + daysofmonths[month1]
just conditionally add to days:
if leap_year(year1) == 366 and month1 == 2:
days = days + 29
else:
days = days + daysofmonths[month1]
then do the same thing lower down with year2 and month2 (I would highly recommend you separate your code into more functions as a lot of it is very repetitive)
This is the only post I found here on this particular problem so I thought I would share my solution.
#days in the months of a non leap year
daysOfMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
#determine if a year is a leap year
def is_leap_year(year1):
year = True
if year1 % 4 != 0:
year = False
elif year1 % 100 != 0:
year = True
elif year1 % 400 != 0:
year = False
else: year = True
return year
#returns the days in the given month of the given year
#I was trying to do something similar to the OP until I read this post
def days_in_month(year, month):
days = 0
if is_leap_year(year) and month == 2:
days += 29
else:
days += daysOfMonths[month - 1]
return days
#itterates through each month starting at year1 month1
#up to but not including month2 of year2 and
#returns the total number of days in that period
def total_days(year1, month1, year2, month2):
days = 0
while year1 < year2 or month1 < month2:
days += days_in_month(year1, month1)
month1 += 1
if month1 == 13:
year1 += 1
month1 = 1
return days
def daysBetweenDates(year1, month1, day1, year2, month2, day2):
days = total_days(year1, month1, year2, month2)
#because I included the beginning month I have to subtract day1
#because I did not include the final month I have to add day2
return days - day1 + day2
#I used print statements here to troubleshoot
#print days_in_month(2012, 1)
#print daysBetweenDates(2012, 1, 1, 2012, 2, 28)
#print daysBetweenDates(2012, 1, 1, 2012, 3, 1)
#print daysBetweenDates(2011,6,30,2012,6,30)
#print daysBetweenDates(2011,1,1,2012,8,8)
#print daysBetweenDates(1900,1,1,1999,12,31)
def test():
test_cases = [((2012,1,1,2012,2,28), 58),
((2012,1,1,2012,3,1), 60),
((2011,6,30,2012,6,30), 366),
((2011,1,1,2012,8,8), 585 ),
((1900,1,1,1999,12,31), 36523)]
for (args, answer) in test_cases:
result = daysBetweenDates(*args)
if result != answer:
print "Test with data:", args, "failed"
else:
print "Test case passed!"
test()

Rendering Text Calendar

I have a function which is supposed to build a header for a calendar like so:
' Sun Mon Tue Wed Thu Fri Sat '
It takes a isoweekday (one of '#Monday' 1, 2, 3, 4, 5, 6, 7 '#Sunday').
Here is the code:
#staticmethod
def _isoweekday_to_str(isoweekday):
isoweekday = isoweekday - 1
isoweekday = str(isoweekday)
x = datetime.strptime(isoweekday, '%w')
return x.strftime('%a')
TEXT_CAL_MONTH_WEEK_HEADER = ""
iter_isoweekday = week_start
for _ in range(0,7):
TEXT_CAL_MONTH_WEEK_HEADER += self._isoweekday_to_str(iter_isoweekday).rjust(TEXT_CAL_CELL_WIDTH, " ")
if iter_isoweekday != 7:
iter_isoweekday += 1
else:
iter_isoweekday = 1
The output I'm getting, when passing in 4 as the week start, is:
' Mon Mon Mon Mon Mon Mon Mon '
it should be:
' Thu Fri Sat Sun Mon Tue Wed '
I'm really, really not sure what's going on. I think it's either something to do with the way variables are assigned, string mutation, or the datetime library.
UPDATE: it appears that datetime.strptime is the problem. No matter what I pass in, I get datetime(1900, 1, 1, 0, 0) back... which, you guessed it, was a Monday.
Help?
You can get the localised days of the week from the calendar module:
>>> import calendar
>>> list(calendar.day_name)
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
Or the abbreviated names, using calendar.day_abbr:
>>> ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
See the documentation if you need other locales.
Obviously, the module can produce whole calendars:
>>> print calendar.TextCalendar().formatmonth(2016, 1)
January 2016
Mo Tu We Th Fr Sa Su
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
I'm not sure why you want to do it this way, but you need an actual date to pass to strptime rather than an isoweekday. For example,
from datetime import datetime
TEXT_CAL_CELL_WIDTH = 5
def _isoweekday_to_str(isoweekday):
isoweekday = '1900-01-{:02d}'.format(isoweekday)
x = datetime.strptime(isoweekday, '%Y-%m-%d')
return x.strftime('%a')
TEXT_CAL_MONTH_WEEK_HEADER = ""
week_start = 4
iter_isoweekday = week_start
for _ in range(0,7):
TEXT_CAL_MONTH_WEEK_HEADER += _isoweekday_to_str(iter_isoweekday).rjust(TEXT_CAL_CELL_WIDTH, " ")
if iter_isoweekday != 7:
iter_isoweekday += 1
else:
iter_isoweekday = 1
print(TEXT_CAL_MONTH_WEEK_HEADER )
Output:
Thu Fri Sat Sun Mon Tue Wed
(This works because 01/01/1900 was a Monday).
But why not do something similar with a dictionary of day names:
day_names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
def weekday_header(week_start):
header = ''.join(['{:4s}'.format(day_names[(day_number+week_start) % 7])
for day_number in range(7)])
return header
print(weekday_header(4))

displaying calendar items closest to today using datetime

I have a dictionary of my calendar items for a month (date as "key", items in the form of a list as "value") that I want to print out a certain way (That dictionary in included in the code, assigned to dct). I only want to display items that are on or after the current date (i.e. today). The display format is:
day: item1, item2
I also want those items to span only 5 lines of stdout with each line 49 characters wide (spaces included). This is necessary because the output will be displayed in conky (app for linux).
Since a day can have multiple agenda items, the output will have to be wrapped and printed out on more than one line. I want the code to account for that by selecting only those days whose items can fit in 5 or less lines instead of printing 5 days with associated items on >5 lines. For e.g.
day1: item1, item2
item3
day2: item1
day3: item1,
item2
Thats 3 days on/after current day printing on 5 lines with each line 49 char wide. Strings exceeding 49 char are wrapped on newline.
Here is the code i've written to do this:
#!/usr/bin/env python
from datetime import date, timedelta, datetime
import heapq
import re
import textwrap
pattern_string = '(1[012]|[1-9]):[0-5][0-9](\\s)?(?i)(am|pm)'
pattern = re.compile(pattern_string)
# Explanation of pattern_string:
# ------------------------------
#( #start of group #1
#1[012] # start with 10, 11, 12
#| # or
#[1-9] # start with 1,2,...9
#) #end of group #1
#: # follow by a semi colon (:)
#[0-5][0-9] # follw by 0..5 and 0..9, which means 00 to 59
#(\\s)? # follow by a white space (optional)
#(?i) # next checking is case insensitive
#(am|pm) # follow by am or pm
# The 12-hour clock format is start from 0-12, then a semi colon (:) and follow by 00-59 , and end with am or pm.
# Time format that match:
# 1. "1:00am", "1:00 am","1:00 AM" ,
# 2. "1:00pm", "1:00 pm", "1:00 PM",
# 3. "12:50 pm"
d = date.today() # datetime.date(2013, 8, 11)
e = datetime.today() # datetime.datetime(2013, 8, 11, 5, 56, 28, 702926)
today = d.strftime('%a %b %d') # 'Sun Aug 11'
dct = {
'Thu Aug 01' : [' Weigh In'],
'Thu Aug 08' : [' 8:00am', 'Serum uric acid test', '12:00pm', 'Make Cheesecake'],
'Sun Aug 11' : [" Awais chotu's birthday", ' Car wash'],
'Mon Aug 12' : ['10:00am', 'Start car for 10 minutes'],
'Thu Aug 15' : [" Hooray! You're Facebook Free!", '10:00am', 'Start car for 10 minutes'],
'Mon Aug 19' : ['10:00am', 'Start car for 10 minutes'],
'Thu Aug 22' : ['10:00am', 'Start car for 10 minutes'],
'Mon Aug 26' : ['10:00am', 'Start car for 10 minutes'],
'Thu Aug 29' : ['10:00am', 'Start car for 10 minutes']
}
def join_time(lst):
'''Searches for a time format string in supplied list and concatenates it + the event next to it as an single item
to a list and returns that list'''
mod_lst = []
for number, item in enumerate(lst):
if re.search(pattern, item):
mod_lst.append(item + ' ' + lst[number+1]) # append the item (i.e time e.g '1:00am') and the item next to it (i.e. event)
del lst[number+1]
else:
mod_lst.append(item)
return mod_lst
def parse_date(datestring):
return datetime.strptime(datestring + ' ' + str(date.today().year), "%a %b %d %Y") # returns a datetime obj for the time string; "Sun Aug 11" = datetime.datetime(1900, 8, 11, 0, 0)
deltas = [] # holds datetime.timedelta() objs; timedelta(days, seconds, microseconds)
val_len = []
key_len = {}
for key in dct:
num = len(''.join(item for item in dct[key]))
val_len.append(num) # calculate the combined len of all items in the
# list which are the val of a key and add them to val_len
if num > 37:
key_len[key] = 2
else:
key_len[key] = 1
# val_len = [31, 9, 61, 31, 31, 49, 31, 32, 31]
# key_len = {'Sun Aug 11': 1, 'Mon Aug 12': 1, 'Thu Aug 01': 1, 'Thu Aug 15': 2, 'Thu Aug 22': 1, 'Mon Aug 19': 1, 'Thu Aug 08': 2, 'Mon Aug 26': 1, 'Thu Aug 29': 1}
counter = 0
for eachLen in val_len:
if eachLen > 37:
counter = counter + 2
else:
counter = counter + 1
# counter = 11
if counter > 5: # because we want only those 5 events in our conky output which are closest to today
n = counter - 5 # n = 6, these no of event lines should be skipped
for key in dct:
deltas.append(e - parse_date(key)) # today - key date (e.g. 'Sun Aug 11') ---> datetime.datetime(2013, 8, 11, 5, 56, 28, 702926) - datetime.datetime(1900, 8, 11, 0, 0)
# TODO: 'n' no of event lines should be skipped, NOT n no of days!
for key in sorted(dct, key=parse_date): # sorted() returns ['Thu Aug 01', 'Thu Aug 08', 'Sun Aug 11', 'Mon Aug 12', 'Thu Aug 15', 'Mon Aug 19', 'Thu Aug 22', 'Mon Aug 26', 'Thu Aug 29']
tdelta = e - parse_date(key)
if tdelta in heapq.nlargest(n, deltas): # heapq.nlargest(x, iterable[, key]); returns list of 'x' no. of largest items in iterable
pass # In this case it should return a list of top 6 largest timedeltas; if the tdelta is in
# that list, it means its not amongst the 5 events we want to print
else:
if key == today:
value = dct[key]
val1 = '${color green}' + key + '$color: '
mod_val = join_time(value)
val2 = textwrap.wrap(', '.join(item for item in mod_val), 37)
print val1 + '${color 40E0D0}' + '$color\n ${color 40E0D0}'.join(item for item in val2) + '$color'
else:
value = dct[key]
mod_val = join_time(value)
output = key + ': ' + ', '.join(item for item in mod_val)
print '\n '.join(textwrap.wrap(output, 49))
else:
for key in sorted(dct, key=parse_date):
if key == today:
value = dct[key]
val1 = '${color green}' + key + '$color: '
mod_val = join_time(value)
val2 = textwrap.wrap(', '.join(item for item in mod_val), 37)
print val1 + '${color 40E0D0}' + '$color\n ${color 40E0D0}'.join(item for item in val2) + '$color'
else:
value = dct[key]
mod_val = join_time(value)
output = key + ': ' + ', '.join(item for item in mod_val)
print '\n '.join(textwrap.wrap(output, 49))
The result is:
Thu Aug 22: 10:00am Start car for 10 minutes
Mon Aug 26: 10:00am Start car for 10 minutes
Thu Aug 29: 10:00am Start car for 10 minutes
I've commented the code heavily so it shouldn't be difficult to figure out how it works. I'm basically calculating the days farthest away from current day using datetime and skipping those days and their items. The code usually works well but once in a while it doesn't. In this case the output should have been:
Mon Aug 19: 10:00am Start car for 10 minutes
Thu Aug 22: 10:00am Start car for 10 minutes
Mon Aug 26: 10:00am Start car for 10 minutes
Thu Aug 29: 10:00am Start car for 10 minutes
since these are the days after the current day (Fri 16 Aug) whose items fit in 5 lines. How do I fix it to skip n no of lines rather than no of days farthest away from today?
I was thinking of using key_len dict to somehow filter the output further, by printing the items of only those days whose items length sum up to < or = 5...
I'm stuck.
It's very hard to tell what you're asking here, and your code is a huge muddle.
However, the reason you're getting the wrong output in the given example is very obvious, and matches the TODO comment in your code, so I'm going to assume that's the only part you're asking about:
# TODO: 'n' no of event lines should be skipped, NOT n no of days!
I don't understand why you want to skip to the last 5 lines after today instead of the first 5, but I'll assume you have some good reason for that.
The easiest way to solve this is to just do them in reverse, prepend the lines to a string instead of printing them directly, stop when you've reached 5 lines, and then print the string. (This would also save the wasteful re-building of the heap over and over, etc.)
For example, something like this:
outlines = []
for key in sorted(dct, key=parse_date, reverse=True): # sorted() returns ['Thu Aug 01', 'Thu Aug 08', 'Sun Aug 11', 'Mon Aug 12', 'Thu Aug 15', 'Mon Aug 19', 'Thu Aug 22', 'Mon Aug 26', 'Thu Aug 29']
if parse_date(key) < parse_date(today):
break
tdelta = e - parse_date(key)
if key == today:
value = dct[key]
val1 = '${color green}' + key + '$color: '
mod_val = join_time(value)
val2 = textwrap.wrap(', '.join(item for item in mod_val), 37)
outstr = val1 + '${color 40E0D0}' + '$color\n ${color 40E0D0}'.join(item for item in val2) + '$color'
outlines[:0] = outstr.splitlines()
else:
value = dct[key]
mod_val = join_time(value)
output = key + ': ' + ', '.join(item for item in mod_val)
outstr = '\n '.join(textwrap.wrap(output, 49))
outlines[:0] = outstr.splitlines()
if len(outlines) >= 5:
break
print '\n'.join(outlines)
There are a lot of ways you could simplify this. For example, instead of passing around string representations of dates and using parse_date all over the place, just pass around dates, and format them once at the end. Use string formatting instead of 120-character multiple-concatenation expressions. Build your data structures once and use them, instead of rebuilding them over and over where you need them. And so on. But this should be all you need to get it to work.

Categories