5
\$\begingroup\$

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.

\$\endgroup\$
3
\$\begingroup\$

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

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.