Last active
January 6, 2025 15:50
-
-
Save rjvitorino/4a8356707be032b82fa419ca37b5f283 to your computer and use it in GitHub Desktop.
Cassidy's interview question of the week: given a year, a script that determines weekdays and dates for US holidays (New Year's, Easter, Memorial Day, Independence Day, Thanksgiving, Christmas)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Holiday date calculation module. | |
This module provides functionality to calculate dates and weekdays for various holidays | |
in the Gregorian calendar, including both fixed-date holidays (like Christmas and New | |
Years Day) and floating holidays (like Easter and Thanksgiving). | |
""" | |
from datetime import date, timedelta | |
from enum import Enum | |
from dataclasses import dataclass | |
from typing import Optional | |
WEEKDAYS = [ | |
"Monday", | |
"Tuesday", | |
"Wednesday", | |
"Thursday", | |
"Friday", | |
"Saturday", | |
"Sunday", | |
] | |
@dataclass | |
class HolidayInfo: | |
"""Information about a holiday. | |
Attributes: | |
name: Display name of the holiday | |
month: Month number (1-12, 0 for special cases like Easter) | |
day: Day of the month for fixed-date holidays | |
weekday: Day of week (0=Monday, 6=Sunday) for floating holidays | |
week_of_month: Week number (1=first, -1=last) for floating holidays | |
fixed_weekday: Name of weekday for holidays always falling on the same weekday | |
""" | |
name: str | |
month: int | |
day: Optional[int] = None | |
weekday: Optional[int] = None # 0=Monday, 6=Sunday | |
week_of_month: Optional[int] = None # 1=first, -1=last | |
fixed_weekday: Optional[str] = None # Holidays always falling on the same weekday | |
class Holiday(Enum): | |
"""Enumeration of supported holidays with their date information.""" | |
NEW_YEARS_DAY = HolidayInfo("New Year's Day", month=1, day=1) | |
INDEPENDENCE_DAY = HolidayInfo("Independence Day", month=7, day=4) | |
CHRISTMAS_DAY = HolidayInfo("Christmas Day", month=12, day=25) | |
THANKSGIVING = HolidayInfo( | |
"Thanksgiving Day", month=11, week_of_month=4, fixed_weekday="Thursday" | |
) # 4th Thursday of November | |
MEMORIAL_DAY = HolidayInfo( | |
"Memorial Day", month=5, weekday=0, week_of_month=-1 | |
) # Last Monday of May | |
EASTER = HolidayInfo( | |
"Easter Sunday", month=0, day=0 | |
) # Special case, requires calculation | |
class HolidayCalculator: | |
"""Calculator for determining dates and weekdays of various holidays.""" | |
_instance = None | |
def __new__(cls): | |
if cls._instance is None: | |
cls._instance = super().__new__(cls) | |
return cls._instance | |
def get_holiday_date(self, holiday: Holiday, year: int) -> date: | |
"""Get the date for a specific holiday in a given year. | |
Args: | |
holiday: The holiday to calculate | |
year: The year for which to calculate the holiday | |
Returns: | |
date: The calculated date of the holiday | |
Raises: | |
ValueError: If the holiday cannot be calculated with the given parameters | |
""" | |
info = holiday.value | |
# Special case for Easter | |
if holiday == Holiday.EASTER: | |
return self._calculate_easter(year) | |
if info.day: # Fixed date holiday | |
return date(year, info.month, info.day) | |
# Convert fixed_weekday to weekday number if specified | |
weekday = ( | |
WEEKDAYS.index(info.fixed_weekday) if info.fixed_weekday else info.weekday | |
) | |
if weekday is not None and info.week_of_month is not None: | |
return self._get_weekday_based_date( | |
year, info.month, weekday, info.week_of_month | |
) | |
raise ValueError(f"Unable to calculate date for {holiday.name}") | |
def get_holiday_weekday(self, holiday: Holiday, year: int) -> str: | |
"""Get the weekday name for a specific holiday in a given year.""" | |
info = holiday.value | |
if info.fixed_weekday: | |
return info.fixed_weekday | |
return WEEKDAYS[self.get_holiday_date(holiday, year).weekday()] | |
@staticmethod | |
def _get_weekday_based_date( | |
year: int, month: int, weekday: int, week_of_month: int | |
) -> date: | |
"""Calculate date for holidays based on weekday and week of month. | |
Args: | |
year: The year for calculation | |
month: The month (1-12) | |
weekday: The day of week (0=Monday, 6=Sunday) | |
week_of_month: Which week (1=first, -1=last) | |
Returns: | |
date: The calculated date | |
""" | |
first_day = date(year, month, 1) | |
if week_of_month == -1: # Last occurrence | |
last_day = ( | |
date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1) | |
) - timedelta(days=1) | |
day = last_day | |
while day.weekday() != weekday: | |
day -= timedelta(days=1) | |
return day | |
# Find first occurrence of weekday | |
day = first_day + timedelta(days=(weekday - first_day.weekday() + 7) % 7) | |
# Add weeks as needed | |
return day + timedelta(weeks=week_of_month - 1) | |
def _calculate_easter(self, year: int) -> date: | |
"""Calculate Easter Sunday using Butcher's Algorithm. | |
This algorithm determines Easter Sunday's date for any year in the Gregorian calendar. | |
Easter falls on the first Sunday following the first ecclesiastical full moon | |
that occurs on or after March 21. | |
Args: | |
year: The year for which to calculate Easter | |
Returns: | |
date: Easter Sunday's date | |
References: | |
- Butcher's Algorithm: https://en.wikipedia.org/wiki/Computus#Butcher's_algorithm | |
""" | |
golden_year = year % 19 # Position in the 19-year Metonic cycle | |
century = year // 100 | |
year_in_century = year % 100 | |
# Calculate corrections | |
leap_years = century // 4 # Number of leap years | |
non_leap = century % 4 | |
lunar_correction = (century + 8) // 25 | |
solar_correction = (century - lunar_correction + 1) // 3 | |
# Calculate moon phase | |
moon_phase = ( | |
19 * golden_year + century - leap_years - solar_correction + 15 | |
) % 30 | |
# Calculate Sunday date | |
century_leap_years = year_in_century // 4 | |
sunday_offset = ( | |
32 | |
+ 2 * non_leap | |
+ 2 * century_leap_years | |
- moon_phase | |
- (year_in_century % 4) | |
) % 7 | |
# Calculate Easter date | |
lunar_offset = (golden_year + 11 * moon_phase + 22 * sunday_offset) // 451 | |
# Final date calculation | |
month = (moon_phase + sunday_offset - 7 * lunar_offset + 114) // 31 | |
day = ((moon_phase + sunday_offset - 7 * lunar_offset + 114) % 31) + 1 | |
return date(year, month, day) | |
# Simplify the public interface using the singleton calculator | |
_calculator = HolidayCalculator() | |
def get_holiday_weekday(holiday: Holiday, year: int) -> str: | |
"""Get the weekday name for any holiday in a given year. | |
Args: | |
holiday: The holiday to query | |
year: The year to check | |
Returns: | |
str: Name of the weekday (e.g., "Monday") | |
""" | |
return _calculator.get_holiday_weekday(holiday, year) | |
def get_holiday_date(holiday: Holiday, year: int) -> str: | |
"""Get the ISO formatted date for any holiday in a given year.""" | |
return _calculator.get_holiday_date(holiday, year).isoformat() | |
def new_years_day(year: int) -> str: | |
"""Get the weekday name for New Year's Day of the given year. | |
This is an example of how to create a simple, specific holiday function | |
while leveraging the more flexible underlying implementation. | |
""" | |
return get_holiday_weekday(Holiday.NEW_YEARS_DAY, year) | |
def main(): | |
# Original requirement test | |
assert new_years_day(2024) == "Monday" | |
assert new_years_day(2025) == "Wednesday" | |
# More comprehensive tests | |
assert get_holiday_weekday(Holiday.NEW_YEARS_DAY, 2024) == "Monday" | |
assert get_holiday_weekday(Holiday.CHRISTMAS_DAY, 2024) == "Wednesday" | |
assert get_holiday_weekday(Holiday.INDEPENDENCE_DAY, 2024) == "Thursday" | |
assert get_holiday_date(Holiday.MEMORIAL_DAY, 2024) == "2024-05-27" | |
assert get_holiday_date(Holiday.THANKSGIVING, 2024) == "2024-11-28" | |
assert get_holiday_date(Holiday.EASTER, 2024) == "2024-03-31" | |
# Assertions for 2025 | |
assert get_holiday_weekday(Holiday.NEW_YEARS_DAY, 2025) == "Wednesday" | |
assert get_holiday_weekday(Holiday.CHRISTMAS_DAY, 2025) == "Thursday" | |
assert get_holiday_weekday(Holiday.INDEPENDENCE_DAY, 2025) == "Friday" | |
assert get_holiday_date(Holiday.MEMORIAL_DAY, 2025) == "2025-05-26" | |
assert get_holiday_date(Holiday.THANKSGIVING, 2025) == "2025-11-27" | |
assert get_holiday_date(Holiday.EASTER, 2025) == "2025-04-20" | |
print("All checks passed!") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment