diff --git a/__main__.py b/__main__.py index 53c2c43..b25a3c5 100644 --- a/__main__.py +++ b/__main__.py @@ -1,5 +1,15 @@ +import doomsday.date as date +import doomsday.algorithm as algorithm + + def main() -> None: - print("Hello world") + date_string: str = input("Date (format YYYY-MM-DD): ") + + if (not date.is_valid_date(date_string)): + return + + weekday_at_given_date: str = algorithm.get_weekday_for_date(date_string) + print(f"The date {date_string} is a {weekday_at_given_date}.") main() diff --git a/doomsday/algorithm.py b/doomsday/algorithm.py index cf81d60..d3ad35b 100644 --- a/doomsday/algorithm.py +++ b/doomsday/algorithm.py @@ -1,2 +1,65 @@ +import math +import doomsday.date as date + +CENTURY_OFFSETS = (2, 0, 5, 3) +DAY_NAMES = ( + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" +) + + def get_weekday_for_date(date: str) -> str: - return "Sunday" + """Returns the name of the day at a given date.""" + date_split: list[str] = date.split("-") + year: int = int(date_split[0]) + month: int = int(date_split[1]) + day: int = int(date_split[2]) + + year_anchor_day: int = get_anchor_day_for_year(year) + month_anchor: int = get_anchor_for_month(month, year) + + # Calculate the number of days between the month's anchor day and + # the wanted day. + day_offset: int = abs(day - month_anchor) + # Since the month's anchor day and the year's anchor day are the same + # we can add the year's anchor day index to get the final day index. + final_day_index = (year_anchor_day + day_offset) % 7 + + return DAY_NAMES[final_day_index] + + +def get_anchor_day_for_year(year: int) -> int: + """Returns the anchor day of a given year starting at 0 for Sunday""" + + # Apply the Odd + 11 algorithm + year_digits: int = year % 100 + if (year_digits % 2 == 1): + year_digits += 11 + + year_digits = year_digits // 2 + if (year_digits % 2 == 1): + year_digits += 11 + + # Calculate the century offset for the current year + multiple_of_7: int = math.ceil(year_digits / 7) * 7 + century_index: int = (year // 100) % 4 + century_offset = CENTURY_OFFSETS[century_index] + + return multiple_of_7 - year_digits + century_offset + + +def get_anchor_for_month(month: int, year: int) -> int: + """Returns the anchor day of a given month given the year""" + + month_anchors = ( + 11 if date.is_leap_year(year) else 10, + 22 if date.is_leap_year(year) else 21, + 0, 4, 9, 6, 11, 8, 7, 10, 7, 12 + ) + + return month_anchors[month - 1] diff --git a/doomsday/date.py b/doomsday/date.py index 0f8e737..f90d907 100644 --- a/doomsday/date.py +++ b/doomsday/date.py @@ -1,2 +1,66 @@ +DAYS_PER_MONTH = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + + def is_valid_date(date: str) -> bool: + """ + Returns whether a date is properly formatted to the YYYY-MM-DD format and + can be ran through the doomsday algorithm. + """ + + split_date: list[str] = date.split("-") + + # Check for correct date format + if (len(split_date) != 3 or "" in split_date): + print("Date must be formatted as YYYY-MM-DD") + return False + + year: int = int(split_date[0]) + month: int = int(split_date[1]) + day: int = int(split_date[2]) + + if (not date_exists(year, month, day)): + return False + + # Years below 1583 are not supported. + if (year < 1583): + print("For simplicity, years before 1583 are not supported.") + return False + return True + + +def date_exists(year: int, month: int, day: int) -> bool: + """ + Checks whether the given date can be ran into the doomsday algorithm. + Invalid dates are dates before 1583-01-01. + """ + + # Check for valid month + if (month < 1 or month > 12): + print("Invalid month") + return False + + # Check for valid day, accounting for leap years. + if (day < 1 or day > get_days_per_month(month, year)): + print("Invalid day number for the given date.") + return False + + return True + + +def get_days_per_month(month: int, year: int) -> int: + """Returns the number of day of the given month, + accounting for leap years""" + + if (month == 2 and is_leap_year(year)): + return DAYS_PER_MONTH[month - 1] + 1 + else: + return DAYS_PER_MONTH[month - 1] + + +def is_leap_year(year: int) -> bool: + """Check if a year is a leap year. + Leap years are every year that are multiples of 4 but not multiples of 100 + or years that are multiple of 400.""" + + return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0