# Weekday Module for meshbot # Provides detailed information about a given date including day of week, day of year, weeks remaining, etc. # Based on ideas from https://github.com/coding-horror/basic-computer-games/blob/main/95_Weekday/python/weekday.py import datetime import calendar from modules.log import logger trap_list_weekday = ("weekday", ) help_text_weekday = "weekday - Get info about today or a date: weekday or weekday MM,DD,YYYY" def is_leap_year(year: int) -> bool: """Check if a year is a leap year.""" if (year % 4) != 0: return False return True if (year % 100) != 0 else year % 400 == 0 def get_day_of_week_name(date_obj: datetime.date) -> str: """Get the day of week name for a given date.""" days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] return days[date_obj.weekday()] def get_week_number(date_obj: datetime.date) -> int: """Get the ISO week number for a given date.""" return date_obj.isocalendar()[1] def is_friday_13th(date_obj: datetime.date) -> bool: """Check if the date is Friday the 13th.""" return date_obj.day == 13 and date_obj.weekday() == 4 # 4 = Friday def get_day_of_year(date_obj: datetime.date) -> int: """Get the day number within the year (1-366).""" return date_obj.timetuple().tm_yday def get_days_left_in_year(date_obj: datetime.date) -> int: """Get the number of days remaining in the year after the given date.""" end_of_year = datetime.date(date_obj.year, 12, 31) delta = end_of_year - date_obj return delta.days def get_days_left_in_month(date_obj: datetime.date) -> int: """Get the number of days remaining in the month after the given date.""" # Get the last day of the month last_day = calendar.monthrange(date_obj.year, date_obj.month)[1] end_of_month = datetime.date(date_obj.year, date_obj.month, last_day) delta = end_of_month - date_obj return delta.days def is_even_week(date_obj: datetime.date) -> bool: """Check if the date falls on an even week number.""" week_num = get_week_number(date_obj) return week_num % 2 == 0 def get_days_till_christmas(date_obj: datetime.date) -> tuple: """Calculate days until Christmas. Returns: A tuple of (days_until, christmas_date) """ # Get Christmas of the current year christmas = datetime.date(date_obj.year, 12, 25) # If Christmas has already passed this year, use next year's Christmas if date_obj > christmas: christmas = datetime.date(date_obj.year + 1, 12, 25) delta = christmas - date_obj return delta.days, christmas def get_days_ago(date_obj: datetime.date) -> tuple: """Calculate how many days and years ago a date was. Returns: A tuple of (days_ago, years_ago) or (0, 0) if date is in the future """ today = datetime.date.today() if date_obj >= today: return 0, 0 delta = today - date_obj days_ago = delta.days years_ago = days_ago // 365 return days_ago, years_ago def get_season(date_obj: datetime.date) -> str: """Get the season for a given date. Returns: A string with the season (Spring, Summer, Fall, Winter) """ month = date_obj.month day = date_obj.day southern_hemisphere = False # Change to True if you want Southern Hemisphere seasons # Northern Hemisphere Seasons # Spring: March 20 - June 20 # Summer: June 21 - September 22 # Fall: September 23 - December 21 # Winter: December 22 - March 19 # Southern Hemisphere Seasons # Spring: September 23 - December 21 # Summer: December 22 - March 19 # Fall: March 20 - June 20 # Winter: June 21 - September 22 if southern_hemisphere: if (month == 9 and day >= 23) or (month in [10, 11]) or (month == 12 and day <= 21): return "๐ŸŒธ Spring" elif (month == 12 and day >= 22) or (month in [1, 2]) or (month == 3 and day <= 19): return "โ˜€๏ธ Summer" elif (month == 3 and day >= 20) or (month in [4, 5]) or (month == 6 and day <= 20): return "๐Ÿ‚ Fall" else: return "โ„๏ธ Winter" else: if (month == 3 and day >= 20) or (month in [4, 5]) or (month == 6 and day <= 20): return "๐ŸŒธ Spring" elif (month == 6 and day >= 21) or (month in [7, 8]) or (month == 9 and day <= 22): return "โ˜€๏ธ Summer" elif (month == 9 and day >= 23) or (month in [10, 11]) or (month == 12 and day <= 21): return "๐Ÿ‚ Fall" else: return "โ„๏ธ Winter" def get_business_days_left(date_obj: datetime.date) -> int: """Get the number of business days (Mon-Fri) remaining in the year. Returns: An integer of business days left """ end_of_year = datetime.date(date_obj.year, 12, 31) business_days = 0 current = date_obj + datetime.timedelta(days=1) # Start from tomorrow while current <= end_of_year: if current.weekday() < 5: # Monday=0, Friday=4 business_days += 1 current += datetime.timedelta(days=1) return business_days def get_week_of_month(date_obj: datetime.date) -> int: """Get which week of the month the date falls on (1-5). Week 1: Days 1-7 Week 2: Days 8-14 etc. Returns: An integer representing the week of month (1-5) """ return (date_obj.day - 1) // 7 + 1 def to_roman_numeral(num: int) -> str: """Convert an integer to Roman numerals. Returns: A string with the Roman numeral representation """ val = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1] syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] roman_num = '' for i in range(len(val)): count = int(num / val[i]) roman_num += syms[i] * count num -= val[i] * count return roman_num def get_days_till_next_holiday(date_obj: datetime.date) -> str: """Get the countdown to the next major holiday. Returns: A string with holiday name and days until it """ # holidays is a list of tuples: (holiday_name, month, day) holidays = [ ("๐ŸฅณNew Year's Day", 1, 1), ("๐Ÿ€St. Patrick's Day", 3, 17), ("๐Ÿ‡บ๐Ÿ‡ธIndependence Day", 7, 4), ("๐ŸŽ„Christmas", 12, 25), ] today = datetime.date.today() next_holiday = None min_days = float('inf') for holiday_name, month, day in holidays: if month is None: # Skip variable holidays like Easter for simplicity continue # Check this year holiday_date = datetime.date(date_obj.year, month, day) if holiday_date >= date_obj: days_until = (holiday_date - date_obj).days if days_until < min_days: min_days = days_until next_holiday = (holiday_name, days_until) # Check next year if we don't find one this year if next_holiday is None: holiday_date = datetime.date(date_obj.year + 1, month, day) days_until = (holiday_date - date_obj).days if days_until < min_days: min_days = days_until next_holiday = (holiday_name, days_until) if next_holiday: return f"{next_holiday[0]}: {next_holiday[1]} days" return "No holiday found" def parse_date_input(date_str: str) -> datetime.date | None: """Parse a date string in MM,DD,YYYY format. Returns: A datetime.date object or None if parsing fails. """ try: parts = date_str.strip().split(',') if len(parts) != 3: return None month, day, year = int(parts[0]), int(parts[1]), int(parts[2]) # Validate date if year < 1901: return None date_obj = datetime.date(year, month, day) return date_obj except (ValueError, TypeError): return None def get_weekday_info(date_obj: datetime.date = None) -> str: """Get comprehensive weekday information for a given date. Args: date_obj: A datetime.date object. If None, uses today's date. Returns: A formatted string with weekday information. """ if date_obj is None: date_obj = datetime.date.today() today = datetime.date.today() day_name = get_day_of_week_name(date_obj) day_of_year = get_day_of_year(date_obj) days_left = get_days_left_in_year(date_obj) days_left_month = get_days_left_in_month(date_obj) week_number = get_week_number(date_obj) is_even = is_even_week(date_obj) week_type = "even" if is_even else "odd" season = get_season(date_obj) leap = "Leap Year ๐ŸŽฏ" if is_leap_year(date_obj.year) else "" business_days = get_business_days_left(date_obj) week_of_month = get_week_of_month(date_obj) # Build the response message msg = f"๐Ÿ“… {date_obj.strftime('%m/%d/%Y')} is a {day_name} of {calendar.month_name[date_obj.month]}" if is_friday_13th(date_obj): msg += " (FRIDAY THE 13TH - BEWARE! ๐Ÿ˜ฑ)" msg += "\n" msg += f"๐Ÿงฎ Day {day_of_year}, of {date_obj.year}\n" msg += f"๐Ÿ“Š Week #{week_number} ({week_type} week). Week {week_of_month} of {calendar.month_name[date_obj.month]}, in {season}\n" msg += f"โณ {days_left} days remain in {date_obj.year} and {business_days} week days\n" msg += f"๐Ÿ—“๏ธ {days_left_month} days left this month\n" if leap: msg += f"๐ŸŽฏ {leap}\n" msg += f"๐ŸŽ‰ {get_days_till_next_holiday(date_obj)}\n" # Add past date info if date_obj < today: days_ago, years_ago = get_days_ago(date_obj) msg += f"โฐ That was {years_ago} years and {days_ago % 365} days ago" elif date_obj == today: pass else: days_future = (date_obj - today).days msg += f"โฐ That's {days_future} days from now" return msg def weekdayHandler(message: str, message_from_id: int, deviceID: str) -> str: """Handle weekday command with optional date parameter. Usage: weekday - Show info for today weekday MM,DD,YYYY - Show info for a specific date Args: message: The full message including the command message_from_id: The ID of the user deviceID: The device ID Returns: A formatted string with weekday information or an error message. """ try: # Extract the date parameter if provided parts = message.split() date_obj = None if len(parts) > 1: # User provided a date date_str = " ".join(parts[1:]) date_obj = parse_date_input(date_str) if date_obj is None: return "โŒ Invalid date format. Use: weekday MM,DD,YYYY\nExample: weekday 03,24,1979" return get_weekday_info(date_obj) except Exception as e: logger.error(f"Error in weekdayHandler: {e}") return f"โŒ Error processing weekday command: {str(e)}"