Source code for x_zic.transitions.calculator

"""
Transition calculator - core DST logic
"""
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
import re
from ..core.models import Zone, Rule
from ..parser.base import BaseParser

[docs]def get_transitions(zone_name: str, start_year: int = 1970, end_year: int = 2030, loader = None) -> List[Dict[str, Any]]: """ Calculate transitions for a zone. Args: zone_name: Zone identifier start_year: Start year end_year: End year loader: File loader Returns: List of transition dictionaries """ # This is a simplified implementation # Full implementation would: # 1. Find the zone definition # 2. Find applicable rules # 3. Calculate transition dates for each year # 4. Convert to UTC timestamps # Placeholder implementation transitions = [] # Mock some transitions for common zones if "New_York" in zone_name: # Mock DST transitions for New York for year in range(start_year, end_year + 1): # Spring forward (2nd Sunday in March) spring_date = _find_nth_weekday(year, 3, 2, 6) # 2nd Sunday transitions.append({ 'zone': zone_name, 'timestamp': datetime(year, 3, spring_date, 2, 0, 0).isoformat(), 'offset_before': '-05:00', 'offset_after': '-04:00', 'is_dst': True, 'abbrev': 'EDT' }) # Fall back (1st Sunday in November) fall_date = _find_nth_weekday(year, 11, 1, 6) # 1st Sunday transitions.append({ 'zone': zone_name, 'timestamp': datetime(year, 11, fall_date, 2, 0, 0).isoformat(), 'offset_before': '-04:00', 'offset_after': '-05:00', 'is_dst': False, 'abbrev': 'EST' }) return sorted(transitions, key=lambda x: x['timestamp'])
def _find_nth_weekday(year: int, month: int, n: int, weekday: int) -> int: """ Find the nth weekday in a month. Args: year: Year month: Month (1-12) n: Which occurrence (1=first, 2=second, etc.) weekday: Weekday (0=Monday, 6=Sunday) Returns: Day of month """ # Find first day of the month first_day = datetime(year, month, 1) # Find first occurrence of the weekday days_to_add = (weekday - first_day.weekday()) % 7 first_occurrence = first_day.day + days_to_add # Calculate nth occurrence target_day = first_occurrence + (n - 1) * 7 # Check if still in same month try: datetime(year, month, target_day) return target_day except ValueError: return target_day - 7 # Use last occurrence if out of bounds
[docs]def calculate_dst_transitions(zone: Zone, rules: List[Rule], year: int) -> List[Dict[str, Any]]: """ Calculate DST transitions for a zone in a specific year. Args: zone: Zone definition rules: Applicable rules year: Year to calculate Returns: List of transition dictionaries """ transitions = [] for rule in rules: if not _rule_applies(rule, year): continue # Calculate transition date transition_date = _calculate_rule_date(rule, year) if transition_date: transitions.append({ 'date': transition_date, 'rule': rule, 'zone': zone }) return transitions
def _rule_applies(rule: Rule, year: int) -> bool: """Check if rule applies to given year""" if rule.to_year == 'max': return year >= rule.from_year else: return rule.from_year <= year <= rule.to_year def _calculate_rule_date(rule: Rule, year: int) -> Optional[datetime]: """Calculate actual date for rule transition""" try: month = _month_to_number(rule.month) day = _parse_day_spec(rule.day, year, month) if day: return datetime(year, month, day) except (ValueError, KeyError): pass return None def _month_to_number(month: str) -> int: """Convert month name to number""" months = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12 } return months[month] def _parse_day_spec(day_spec: str, year: int, month: int) -> Optional[int]: """Parse day specification like 'Sun>=8', 'lastSun', '15'""" if day_spec.isdigit(): return int(day_spec) # Handle lastSun, lastMon, etc. if day_spec.startswith('last'): weekday = _weekday_to_number(day_spec[4:]) return _find_last_weekday(year, month, weekday) # Handle Sun>=8, Sun<=25, etc. match = re.match(r'(\w{3})([<>]=)?(\d+)', day_spec) if match: weekday_name, comparator, day_num = match.groups() weekday = _weekday_to_number(weekday_name) base_day = int(day_num) if comparator == '>=': return _find_weekday_on_or_after(year, month, base_day, weekday) elif comparator == '<=': return _find_weekday_on_or_before(year, month, base_day, weekday) return None def _weekday_to_number(weekday: str) -> int: """Convert weekday name to number (0=Monday, 6=Sunday)""" weekdays = { 'Mon': 0, 'Tue': 1, 'Wed': 2, 'Thu': 3, 'Fri': 4, 'Sat': 5, 'Sun': 6 } return weekdays[weekday] def _find_last_weekday(year: int, month: int, weekday: int) -> int: """Find last occurrence of weekday in month""" # Start from last day of month if month == 12: last_day = datetime(year + 1, 1, 1) - timedelta(days=1) else: last_day = datetime(year, month + 1, 1) - timedelta(days=1) # Go backwards to find the weekday days_back = (last_day.weekday() - weekday) % 7 return last_day.day - days_back def _find_weekday_on_or_after(year: int, month: int, day: int, weekday: int) -> int: """Find first weekday on or after given day""" base_date = datetime(year, month, day) days_to_add = (weekday - base_date.weekday()) % 7 return base_date.day + days_to_add def _find_weekday_on_or_before(year: int, month: int, day: int, weekday: int) -> int: """Find last weekday on or before given day""" base_date = datetime(year, month, day) days_to_subtract = (base_date.weekday() - weekday) % 7 return base_date.day - days_to_subtract