# prediction_controller.py from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta from typing import List, Tuple, Dict, Optional import math from calendar_manager import CalendarManager from date_calculator import DateCalculator from event_type_handler import EventTypeHandler from config import EventConfig class PredictionController: """Handles prediction calculation with proper business logic orchestration""" def __init__(self, calendar_manager: CalendarManager, date_calculator: DateCalculator): self.calendar_manager = calendar_manager self.date_calculator = date_calculator self.launch_date: Optional[datetime] = None self.duration: Optional[relativedelta] = None self.prediction: Optional[datetime] = None self.corrected_events: List[Tuple[datetime, datetime, str]] = [] def set_parameters(self, launch_date: str, duration_years: int) -> bool: """Set launch date and duration with validation""" try: self.launch_date = datetime.fromisoformat(launch_date) self.duration = relativedelta(years=duration_years) # Validate parameters if duration_years <= 0: raise ValueError("Duration must be positive") if self.launch_date < datetime(1900, 1, 1): raise ValueError("Launch date too far in the past") if self.launch_date > datetime(2100, 1, 1): raise ValueError("Launch date too far in the future") return True except ValueError as e: print(f"Error setting parameters: {e}") return False def make_prediction(self, launch_date: str, duration_years: int) -> bool: """Calculate prediction with proper business logic orchestration""" if not self.set_parameters(launch_date, duration_years): return False try: # Calculate base prediction (launch + duration - 1 day) prediction_start = self.launch_date + self.duration - timedelta(days=1) # Categorize events by type categorized_events = EventTypeHandler.categorize_events(self.calendar_manager.entries) # Process events according to business rules full_projects = [] half_projects = [] events = [] for event_type in ["EZ 100%", "EZ pauschal"]: if event_type in categorized_events: full_projects.extend(categorized_events[event_type]) if "EZ 50%" in categorized_events: half_projects.extend(categorized_events["EZ 50%"]) if "Sonstige" in categorized_events: events.extend(categorized_events["Sonstige"]) sorted_projects = DateCalculator.sort_periods(full_projects) sorted_half_projects = DateCalculator.sort_periods(half_projects) sorted_events = DateCalculator.sort_periods(events) considered_events = DateCalculator.truncate_periods(sorted_events, self.launch_date) considered_full_projects = DateCalculator.truncate_periods(sorted_projects, self.launch_date) considered_half_projects = DateCalculator.truncate_periods(sorted_half_projects, self.launch_date) considered_full_projects_rounded, months = DateCalculator.round_periods(considered_full_projects) non_overlapping_half_projects = [] for test_interval in considered_half_projects: non_overlapping_half_projects.extend( DateCalculator.find_non_overlapping_periods(considered_full_projects_rounded, test_interval) ) considered_half_projects_rounded, months2 = DateCalculator.round_periods(non_overlapping_half_projects) all_projects_merged = DateCalculator.sort_periods(considered_full_projects_rounded + considered_half_projects_rounded) merged_event_periods = DateCalculator.adjust_periods(considered_events) non_overlapping_event_periods = [] for test_interval in merged_event_periods: non_overlapping_event_periods.extend( DateCalculator.find_non_overlapping_periods(all_projects_merged, test_interval) ) total_months = months + math.ceil(months2 / 2) total_days = sum((end - start).days + 1 for start, end, _ in non_overlapping_event_periods) prediction = self.launch_date + self.duration + relativedelta(months=total_months) + timedelta(days=total_days-1) # Calculate final prediction max_prediction = prediction_start + EventConfig.get_max_prediction_duration() self.prediction = min(prediction, max_prediction) # Collect corrected events from all categories all_corrected_events = considered_full_projects_rounded + considered_half_projects_rounded + non_overlapping_event_periods # Apply corrections to calendar entries self.calendar_manager.correct_dates(all_corrected_events) return True except Exception as e: print(f"Error calculating prediction: {e}") return False def get_launch_date(self) -> Optional[datetime]: """Get the launch date""" return self.launch_date def get_duration(self) -> Optional[relativedelta]: """Get the duration""" return self.duration def get_prediction(self) -> Optional[datetime]: """Get the prediction""" return self.prediction def validate_prediction_inputs(self, launch_date: str, duration_years: int) -> List[str]: """Validate prediction inputs and return list of errors""" errors = [] try: launch_dt = datetime.fromisoformat(launch_date) except ValueError: errors.append("Invalid launch date format") return errors if duration_years <= 0: errors.append("Duration must be positive") if launch_dt < datetime(1900, 1, 1): errors.append("Launch date too far in the past") if launch_dt > datetime(2100, 1, 1): errors.append("Launch date too far in the future") return errors def save_complete_state(self, filename: str) -> bool: """Save the complete prediction state including all calculated data""" from prediction_state_service import PredictionStateService return PredictionStateService.save_prediction_state( self.calendar_manager, self, filename ) def load_complete_state(self, filename: str) -> bool: """Load complete prediction state and restore controller""" from prediction_state_service import PredictionStateService state = PredictionStateService.load_prediction_state(filename) if state: success = PredictionStateService.restore_from_state( state, self.calendar_manager, self ) if success and state.prediction_date: # Restore prediction date directly if available self.prediction = state.prediction_date return success return False