| 1 | from datetime import datetime, timedelta |
| 2 | |
| 3 | def format_datetime(dt): |
| 4 | """Format datetime to ISO string format expected by tests.""" |
| 5 | return dt.strftime("%Y-%m-%dT%H:%M:%S") |
| 6 | |
| 1 | 7 | def delivery_date(start, description): |
| 2 | | pass |
| 8 | # Edge Case: Invalid or unrecognized delivery descriptions |
| 9 | if not isinstance(description, str) or not description: |
| 10 | raise ValueError("Invalid delivery description") |
| 11 | |
| 12 | # Edge Case: Invalid start time - handle both string and datetime input |
| 13 | if isinstance(start, str): |
| 14 | try: |
| 15 | start = datetime.fromisoformat(start) |
| 16 | except ValueError: |
| 17 | raise ValueError("Invalid start time format") |
| 18 | elif not isinstance(start, datetime): |
| 19 | raise ValueError("Invalid start time") |
| 20 | |
| 21 | # Handle fixed delivery date descriptions |
| 22 | if description == "NOW": |
| 23 | return format_datetime(start + timedelta(hours=2)) |
| 24 | |
| 25 | if description == "ASAP": |
| 26 | if start.hour < 13: |
| 27 | # Today at 17:00 |
| 28 | return format_datetime(start.replace(hour=17, minute=0, second=0, microsecond=0)) |
| 29 | else: |
| 30 | # Tomorrow at 13:00 |
| 31 | next_day = start + timedelta(days=1) |
| 32 | return format_datetime(next_day.replace(hour=13, minute=0, second=0, microsecond=0)) |
| 33 | |
| 34 | if description == "EOW": |
| 35 | weekday = start.weekday() # Monday is 0, Sunday is 6 |
| 36 | if weekday in [0, 1, 2]: # Monday, Tuesday, or Wednesday |
| 37 | # Find next Friday (weekday 4) |
| 38 | days_until_friday = 4 - weekday |
| 39 | delivery = start + timedelta(days=days_until_friday) |
| 40 | return format_datetime(delivery.replace(hour=17, minute=0, second=0, microsecond=0)) |
| 41 | elif weekday in [3, 4]: # Thursday or Friday |
| 42 | # Find next Sunday (weekday 6) |
| 43 | days_until_sunday = (6 - weekday) % 7 |
| 44 | if days_until_sunday == 0: |
| 45 | # If it's Sunday, go to next Sunday |
| 46 | days_until_sunday = 7 |
| 47 | delivery = start + timedelta(days=days_until_sunday) |
| 48 | return format_datetime(delivery.replace(hour=20, minute=0, second=0, microsecond=0)) |
| 49 | else: # Saturday or Sunday |
| 50 | # Find next Friday (weekday 4) |
| 51 | days_until_friday = (4 - weekday) % 7 |
| 52 | if days_until_friday == 0: |
| 53 | # If it's Friday, go to next Friday |
| 54 | days_until_friday = 7 |
| 55 | delivery = start + timedelta(days=days_until_friday) |
| 56 | return format_datetime(delivery.replace(hour=17, minute=0, second=0, microsecond=0)) |
| 57 | |
| 58 | # Handle variable delivery date descriptions |
| 59 | if description.endswith('M') and description[:-1].isdigit(): |
| 60 | n = int(description[:-1]) |
| 61 | # Edge Case: Invalid month number |
| 62 | if n < 1 or n > 12: |
| 63 | raise ValueError("Invalid month number") |
| 64 | |
| 65 | # Determine target month and year |
| 66 | current_month = start.month |
| 67 | current_year = start.year |
| 68 | |
| 69 | if current_month < n: |
| 70 | target_year = current_year |
| 71 | target_month = n |
| 72 | else: |
| 73 | target_year = current_year + 1 |
| 74 | target_month = n |
| 75 | |
| 76 | # Find first workday of target month |
| 77 | target_date = datetime(target_year, target_month, 1, 8, 0, 0) |
| 78 | # If first day is weekend, move to next Monday |
| 79 | weekday = target_date.weekday() |
| 80 | if weekday == 5: # Saturday |
| 81 | target_date += timedelta(days=2) |
| 82 | elif weekday == 6: # Sunday |
| 83 | target_date += timedelta(days=1) |
| 84 | |
| 85 | return format_datetime(target_date) |
| 86 | |
| 87 | if description.startswith('Q') and description[1:].isdigit(): |
| 88 | n = int(description[1:]) |
| 89 | # Edge Case: Invalid quarter number |
| 90 | if n < 1 or n > 4: |
| 91 | raise ValueError("Invalid quarter number") |
| 92 | |
| 93 | # Determine target quarter and year |
| 94 | current_month = start.month |
| 95 | current_year = start.year |
| 96 | current_quarter = (current_month - 1) // 3 + 1 |
| 97 | |
| 98 | if current_quarter <= n: |
| 99 | target_year = current_year |
| 100 | target_quarter = n |
| 101 | else: |
| 102 | target_year = current_year + 1 |
| 103 | target_quarter = n |
| 104 | |
| 105 | # Find last workday of target quarter |
| 106 | # End month of quarter |
| 107 | end_month = target_quarter * 3 |
| 108 | # Last day of that month |
| 109 | if end_month in [1, 3, 5, 7, 8, 10, 12]: |
| 110 | last_day = 31 |
| 111 | elif end_month in [4, 6, 9, 11]: |
| 112 | last_day = 30 |
| 113 | else: # February |
| 114 | # Check for leap year |
| 115 | if target_year % 4 == 0 and (target_year % 100 != 0 or target_year % 400 == 0): |
| 116 | last_day = 29 |
| 117 | else: |
| 118 | last_day = 28 |
| 119 | |
| 120 | target_date = datetime(target_year, end_month, last_day, 8, 0, 0) |
| 121 | # If last day is weekend, move to previous Friday |
| 122 | weekday = target_date.weekday() |
| 123 | if weekday == 5: # Saturday |
| 124 | target_date -= timedelta(days=1) |
| 125 | elif weekday == 6: # Sunday |
| 126 | target_date -= timedelta(days=2) |
| 127 | |
| 128 | return format_datetime(target_date) |
| 129 | |
| 130 | # Edge Case: Unrecognized delivery description pattern |
| 131 | raise ValueError("Unrecognized delivery description") |
| 132 | |
| 133 | # Handled Edge Cases: Invalid or unrecognized delivery descriptions, Invalid start time, Invalid month number, Invalid quarter number, Unrecognized delivery description pattern |