Python Day Finder

I have made a Python program to find the day of the week of an inputted date. I did not want to use the time or datetime module, like in this answer, as I found that a bit too easy. (I like to challenge myself)

I want to improve on appropriately putting certain tasks in functions, so my program is easier to read and follow. I would like to know if the logic in my program makes sense, and if there is anything that could have been done easier. If things could have been in a more "Pythonic" way, that would be nice to know too.

Here is the code -

month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
day_names = ["Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"]

SYEAR = 2016
SMONTH = 1
SDAY = 1

def is_leap_year(year):
if year % 4 == 0:
if year % 100 == 0:
if year % 400 == 0:
return True
else:
return False
else:
return True

return False

def calc_year(year):
days = 0
tyear = SYEAR
while not tyear == year:
if tyear < year:
if is_leap_year(tyear):
days += 366
else:
days += 365
tyear += 1

if tyear > year:
if is_leap_year(tyear - 1):
days -= 366
else:
days -= 365

tyear -= 1

return days

def calc_month(month, year):
days = 0
tmonth = SMONTH
while not tmonth == month:
if tmonth == 2 and is_leap_year(year):
days += 1

days += month_days[tmonth - 1]
tmonth += 1

return days

def calc_day(day):
days = 0
tday = SDAY
while not tday == day:
tday += 1
days += 1

return days

def find_day(date):
total = calc_month(date[0], date[2]) + calc_day(date[1]) + calc_year(date[2])
if total < 0:
return day_names[total % -7]
else:
return day_names[total % 7]

def main():
date = input("Enter a day like so <MM, DD, YYYY>: ").split()

month = int(date[0])
day = int(date[1])
year = int(date[2])

if month > 12 or month <= 0:
print("Invalid month")

elif day > month_days[month - 1] or day <= 0:
if not (day == 29 and month == 2):
print("Invalid day")

else:
print(find_day((month, day, year)))

if __name__ == "__main__":
main()


I purposely start with the date 01 01 2016, because it is a leap year and (so far) has not had an effect on the outcome of the program.

Your code looks quite good. You mostly stick to PEP8 though you might want to implement a distance of two empty lines in-between functions on module-level.

Regarding code optimization you may want to make month_days and day_names tuples since they do not change and thus may be implemented as invariant constants. While doing this you can also capitalize their names to indicate that they are global constants as you did with the other module-level variables:

MONTH_DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
DAY_NAMES = ("Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday")


Furthermore, you can make is_leap_year a one-liner, since the if-else blocks and zero-comparisons are a bit verbose for my taste of "pythonicness":

def is_leap_year(year):
return not year % 4 and (year % 100 or not year % 400)


Also I find it a bit intricately, that you pass a tuple date to find_day and then access its members via indexes. I suggest changing that to:

def find_day(month, day, year):
total = calc_month(month, year) + calc_day(day) + calc_year(year)
if total < 0:
return day_names[total % -7]
else:
return day_names[total % 7]

def main():
date = input("Enter a day like so <MM, DD, YYYY>: ").split()

month = int(date[0])
day = int(date[1])
year = int(date[2])

if month > 12 or month <= 0:
print("Invalid month")

elif day > month_days[month - 1] or day <= 0:
if not (day == 29 and month == 2):
print("Invalid day")

else:
print(find_day(month, day, year))


However, if you really want to handle date as such a tuple for later re-use, I recommend that you use tuple unpacking in find_day instead of index accessing for better readability:

def find_day(date):
month, day, year = date
total = calc_month(month, year) + calc_day(day) + calc_year(year)
if total < 0:
return day_names[total % -7]
else:
return day_names[total % 7]

• Instead of creating an is_leap_year function by yourself, you could also recommend the calendar module. – яүυк Jan 11 '17 at 9:22
• @Dex'ter He does want to re-invent the wheel and explicitely stated that he does not want to use any library. – Richard Neumann Jan 11 '17 at 9:25
• Oh, I didn't read through everything he wrote :) – яүυк Jan 11 '17 at 9:26

If you are already reinventing the wheel regarding datetime, I would take their interface as a bit of an example. I would propose to write a Date class, with the ability to parse your input format.

For determining the weekday, I would use a similar strategy as datetime uses, namely convert the dates into ordinals (dates since 0001-01-01) and then use the fact that 0001-01-01 was a Monday, conveniently, which means you can directly use that as index (mod 7).

import re
from itertools import accumulate

class Date(object):
format = re.compile('(\d{1,2}),? (\d{1,2}),? (\d{4})')

month_days = (None, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
days_before = (None, 0) + tuple(accumulate(month_days[1:-1]))
day_names = ("Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday")

def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
assert year >= 1, "Invalid year"
assert 1 <= month <= 12, "Invalid month"
max_days = self.month_days[month] + (month == 2 and self.is_leap())
assert 1 <= day <= max_days, "Invalid day"

def __repr__(self):
return "Date({self.year}, {self.month}, {self.day})".format(self=self)

def is_leap(self):
year = self.year
return bool(not year % 4 and (year % 100 or not year % 400))

def _days_before_year(self):
"year -> number of days before January 1st of year."
y = self.year - 1
return y * 365 + y // 4 - y // 100 + y // 400

def _days_before_month(self):
"year, month -> number of days in year preceeding first day of month."
return self.days_before[self.month] + (self.month > 2 and self.is_leap())

def to_ordinal(self):
"year, month, day -> ordinal, considering 01-Jan-0001 as day 0."
return (self._days_before_year() +
self._days_before_month() +
self.day)

@property
def weekday(self):
# 0001-01-01 was a Monday
return self.day_names[self.to_ordinal() - 1 % 7]

@classmethod
def strptime(cls, date_str):
match = cls.format.match(date_str)
if match is not None:
day, month, year = map(int, match.groups())
else:
raise ValueError("Could not match {}".format(date_str))
return cls(year, month, day)

def main():
date = Date.strptime(input("Enter a day like so <MM DD YYYY>: "))
print(date.weekday)

if __name__ == "__main__":
main()


I made the hard-coded day names etc all tuples, because they don't change.