1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
|
# 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
processed_events = self._process_events_by_type(categorized_events)
# Calculate final prediction
self.prediction = self._calculate_final_prediction(prediction_start, processed_events)
# Apply corrections to calendar entries
self._apply_corrections_to_calendar()
return True
except Exception as e:
print(f"Error calculating prediction: {e}")
return False
def _process_events_by_type(self, categorized_events: Dict[str, List[Tuple[datetime, datetime, str]]]) -> Dict[str, List[Tuple[datetime, datetime, str]]]:
"""Process events according to business rules"""
processed = {}
# Process full-time projects first (EZ 100% and EZ pauschal)
full_projects = []
for event_type in ["EZ 100%", "EZ pauschal"]:
if event_type in categorized_events:
full_projects.extend(categorized_events[event_type])
if full_projects:
processed["full_projects"] = self._process_full_projects(full_projects)
# Process half-time projects (EZ 50%)
if "EZ 50%" in categorized_events:
processed["half_projects"] = self._process_half_projects(
categorized_events["EZ 50%"],
processed.get("full_projects", [])
)
# Process other events (Sonstige)
if "Sonstige" in categorized_events:
processed["other_events"] = self._process_other_events(
categorized_events["Sonstige"],
processed.get("full_projects", []),
processed.get("half_projects", [])
)
return processed
def _process_full_projects(self, full_projects: List[Tuple[datetime, datetime, str]]) -> List[Tuple[datetime, datetime, str]]:
"""Process full-time projects"""
# Sort and truncate periods
sorted_projects = DateCalculator.sort_periods(full_projects)
considered_projects = DateCalculator.truncate_periods(sorted_projects, self.launch_date)
# Filter out any invalid periods
valid_projects = DateCalculator.filter_valid_periods(considered_projects)
# Round to month boundaries
rounded_projects, total_months = DateCalculator.round_periods(valid_projects)
return rounded_projects
def _process_half_projects(self, half_projects: List[Tuple[datetime, datetime, str]],
full_projects: List[Tuple[datetime, datetime, str]]) -> List[Tuple[datetime, datetime, str]]:
"""Process half-time projects"""
# Sort and truncate periods
sorted_projects = DateCalculator.sort_periods(half_projects)
considered_projects = DateCalculator.truncate_periods(sorted_projects, self.launch_date)
# Filter out any invalid periods
valid_projects = DateCalculator.filter_valid_periods(considered_projects)
# Find non-overlapping periods with full projects
non_overlapping_projects = []
for test_interval in valid_projects:
non_overlapping_results = DateCalculator.find_non_overlapping_periods(full_projects, test_interval)
non_overlapping_projects.extend(non_overlapping_results)
# Filter valid periods again after overlap processing
valid_non_overlapping = DateCalculator.filter_valid_periods(non_overlapping_projects)
# Round to month boundaries
rounded_projects, total_months = DateCalculator.round_periods(valid_non_overlapping)
return rounded_projects
def _process_other_events(self, other_events: List[Tuple[datetime, datetime, str]],
full_projects: List[Tuple[datetime, datetime, str]],
half_projects: List[Tuple[datetime, datetime, str]]) -> List[Tuple[datetime, datetime, str]]:
"""Process other events (Sonstige)"""
# Sort and truncate periods
sorted_events = DateCalculator.sort_periods(other_events)
considered_events = DateCalculator.truncate_periods(sorted_events, self.launch_date)
# Filter out any invalid periods
valid_events = DateCalculator.filter_valid_periods(considered_events)
# Find non-overlapping periods with all projects
all_projects = DateCalculator.sort_periods(full_projects + half_projects)
non_overlapping_events = []
for test_interval in valid_events:
non_overlapping_results = DateCalculator.find_non_overlapping_periods(all_projects, test_interval)
non_overlapping_events.extend(non_overlapping_results)
# Filter valid periods again after overlap processing
valid_non_overlapping = DateCalculator.filter_valid_periods(non_overlapping_events)
# Adjust overlapping periods
adjusted_events = DateCalculator.adjust_periods(valid_non_overlapping)
# Final validation to ensure all periods are valid
final_valid_events = DateCalculator.filter_valid_periods(adjusted_events)
return final_valid_events
def _calculate_final_prediction(self, prediction_start: datetime,
processed_events: Dict[str, List[Tuple[datetime, datetime, str]]]) -> datetime:
"""Calculate the final prediction date"""
# Calculate months from projects
total_months = 0
# Full projects count as full months
if "full_projects" in processed_events:
total_months += DateCalculator.calculate_total_months(processed_events["full_projects"])
# Half projects count as half months
if "half_projects" in processed_events:
half_months = DateCalculator.calculate_total_months(processed_events["half_projects"])
total_months += math.ceil(half_months / 2)
# Other events count as days
total_days = 0
if "other_events" in processed_events:
total_days = DateCalculator.calculate_total_days(processed_events["other_events"])
# Calculate final prediction
final_prediction = (prediction_start +
relativedelta(months=total_months) +
timedelta(days=total_days))
# Apply maximum limit
max_prediction = prediction_start + EventConfig.get_max_prediction_duration()
final_prediction = DateCalculator.min_date(final_prediction, max_prediction)
return final_prediction
def _apply_corrections_to_calendar(self):
"""Apply corrected dates to calendar entries and calculate time_period"""
# Collect all corrected events from processed results
all_corrected_events = []
# Get processed events from the prediction calculation
categorized_events = EventTypeHandler.categorize_events(self.calendar_manager.entries)
processed_events = self._process_events_by_type(categorized_events)
# Collect corrected events from all categories
for event_type, events in processed_events.items():
all_corrected_events.extend(events)
# Apply corrections to calendar entries
self.calendar_manager.correct_dates(all_corrected_events)
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
|