from datetime import date, timedelta import random def is_leap_year(year): """Determines if a year is a leap year.""" return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) def days_in_year(year): """Returns the number of days in a given year.""" return 366 if is_leap_year(year) else 365 def days_in_month(year, month): """Returns the number of days in a specific month of a year.""" if month == 2: return 29 if is_leap_year(year) else 28 elif month in [4, 6, 9, 11]: return 30 else: return 31 def get_random_date(start_year=2000, end_year=2030): """Generates a random date within a given year range.""" year = random.randint(start_year, end_year) month = random.randint(1, 12) day = random.randint(1, days_in_month(year, month)) return date(year, month, day) def get_random_date_period(min_days=30, max_days=730, base_year_range=(1990, 2025)): """ Generates a random start date and an end date that is min_days to max_days after the start date. Returns (start_date, end_date, number_of_days). """ start_date = get_random_date(base_year_range[0], base_year_range[1]) num_days_in_period = random.randint(min_days, max_days) end_date = start_date + timedelta(days=num_days_in_period) # The actual number of days between two dates, inclusive of start and exclusive of end, # or inclusive of both if counting "duration". For interest calculations, it's often # the difference as calculated by date objects. # For exact simple interest, the problem usually implies counting the actual days in the period. # timedelta already gives the difference. If the problem implies "from date X to date Y inclusive", # then it might be (end_date - start_date).days + 1. # However, the sample problems (e.g. Dec 27 to Mar 23) count days like: # Dec: 31-27 = 4. Jan: 31. Feb: 28. Mar: 23. Total = 86. # (date(2003,3,23) - date(2002,12,27)).days = 86. So timedelta is correct. actual_days_difference = (end_date - start_date).days return start_date, end_date, actual_days_difference def calculate_exact_days(start_date_obj, end_date_obj): """ Calculates the exact number of days between two date objects. This is consistent with how financial day counts are often done. """ if not isinstance(start_date_obj, date) or not isinstance(end_date_obj, date): raise ValueError("Inputs must be datetime.date objects.") if start_date_obj > end_date_obj: raise ValueError("Start date cannot be after end date.") return (end_date_obj - start_date_obj).days def format_date_for_display(date_obj): """Formats a date object as 'Month Day, Year' (e.g., January 1, 2023).""" if not isinstance(date_obj, date): return str(date_obj) return date_obj.strftime("%B %d, %Y") if __name__ == '__main__': print("Testing Date Utilities:") # Test is_leap_year print(f"\n--- Testing is_leap_year ---") print(f"Is 2000 a leap year? {is_leap_year(2000)} (Expected: True)") assert is_leap_year(2000) print(f"Is 1900 a leap year? {is_leap_year(1900)} (Expected: False)") assert not is_leap_year(1900) print(f"Is 2023 a leap year? {is_leap_year(2023)} (Expected: False)") assert not is_leap_year(2023) print(f"Is 2024 a leap year? {is_leap_year(2024)} (Expected: True)") assert is_leap_year(2024) # Test days_in_year print(f"\n--- Testing days_in_year ---") print(f"Days in 2023: {days_in_year(2023)} (Expected: 365)") assert days_in_year(2023) == 365 print(f"Days in 2024: {days_in_year(2024)} (Expected: 366)") assert days_in_year(2024) == 366 # Test days_in_month print(f"\n--- Testing days_in_month ---") print(f"Days in Feb 2023: {days_in_month(2023, 2)} (Expected: 28)") assert days_in_month(2023, 2) == 28 print(f"Days in Feb 2024: {days_in_month(2024, 2)} (Expected: 29)") assert days_in_month(2024, 2) == 29 print(f"Days in Apr 2023: {days_in_month(2023, 4)} (Expected: 30)") assert days_in_month(2023, 4) == 30 print(f"Days in Jan 2023: {days_in_month(2023, 1)} (Expected: 31)") assert days_in_month(2023, 1) == 31 # Test get_random_date print(f"\n--- Testing get_random_date ---") for _ in range(3): rd = get_random_date(2020, 2022) print(f"Random date: {rd} ({format_date_for_display(rd)})") assert 2020 <= rd.year <= 2022 # Test get_random_date_period print(f"\n--- Testing get_random_date_period ---") for _ in range(3): start, end, num_days = get_random_date_period(min_days=10, max_days=90) print(f"Period: {format_date_for_display(start)} to {format_date_for_display(end)}, Days: {num_days}") assert 10 <= num_days <= 90 assert (end - start).days == num_days # Test calculate_exact_days (matches sample problem from notes) # Problem 3: December 27, 2002 to March 23, 2003 -> 86 days print(f"\n--- Testing calculate_exact_days (Sample Problem) ---") d1 = date(2002, 12, 27) d2 = date(2003, 3, 23) exact_days_sample = calculate_exact_days(d1, d2) print(f"Days from {format_date_for_display(d1)} to {format_date_for_display(d2)}: {exact_days_sample} (Expected: 86)") assert exact_days_sample == 86 # Problem 5: February 14, 1984 to November 30, 1984 -> 290 days (1984 is leap) d3 = date(1984, 2, 14) d4 = date(1984, 11, 30) exact_days_sample2 = calculate_exact_days(d3, d4) print(f"Days from {format_date_for_display(d3)} to {format_date_for_display(d4)}: {exact_days_sample2} (Expected: 290)") assert exact_days_sample2 == 290 print("\nAll date utility tests passed if no assertion errors.")