from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta import math class DateCalculator: @staticmethod def sort_periods(periods): return sorted(periods, key=lambda p: (p[0], p[1])) @staticmethod def truncate_periods(periods, launch): considered_periods = [] for start, end, id in periods: # print(start) # print(launch) truncated_start = max(start, launch) if truncated_start <= end: considered_periods.append((truncated_start, end, id)) return considered_periods @staticmethod def round_periods(periods): rounded_periods = [] total_months = 0 last_end = None for start, end, id in periods: if last_end and start <= last_end: start = last_end + timedelta(days=1) if start > end: continue year_diff = end.year - start.year month_diff = end.month - start.month months = year_diff * 12 + month_diff if end.day >= start.day: months += 1 rounded_end = start + relativedelta(months=months) - timedelta(days=1) rounded_periods.append((start, rounded_end, id)) total_months += months last_end = rounded_end return rounded_periods, total_months @staticmethod def adjust_periods(periods): """Adjust overlapping periods without merging. - Later periods overlapping with a previous one have their start moved to the previous end + 1 day. - Periods fully contained in a previous one are discarded. """ if not periods: return [] adjusted = [] for start, end, pid in periods: if not adjusted: adjusted.append((start, end, pid)) continue last_start, last_end, last_pid = adjusted[-1] if start <= last_end: # Fully contained in previous period → discard if end <= last_end: continue # Overlaps head; push start to the day after last_end new_start = last_end + timedelta(days=1) if new_start <= end: adjusted.append((new_start, end, pid)) # else new_start > end → discard else: adjusted.append((start, end, pid)) return adjusted @staticmethod def find_non_overlapping_periods(existing_periods, test_period): test_start, test_end, id = test_period non_overlapping_periods = [] for start, end, _ in existing_periods: if test_end < start: non_overlapping_periods.append((test_start, test_end, id)) return non_overlapping_periods elif test_start > end: continue else: if test_start < start: non_overlapping_periods.append((test_start, start - timedelta(days=1), id)) test_start = end + timedelta(days=1) if test_start <= test_end: non_overlapping_periods.append((test_start, test_end, id)) return non_overlapping_periods @staticmethod def calculate_prediction(launch_date, duration, **kwargs): prediction_start = launch_date + duration - timedelta(days = 1) events = [] half_projects = [] full_projects = [] other_kwargs = {} for k, v in kwargs.items(): if k == "Sonstige": events.extend(v) elif k == "EZ 50%": half_projects.extend(v) elif k == "EZ 100%": full_projects.extend(v) elif k == "EZ pauschal": full_projects.extend(v) else: other_kwargs[k] = v events = DateCalculator.sort_periods(events) half_projects = DateCalculator.sort_periods(half_projects) full_projects = DateCalculator.sort_periods(full_projects) considered_events = DateCalculator.truncate_periods(events, launch_date) considered_full_projects = DateCalculator.truncate_periods(full_projects, launch_date) considered_half_projects = DateCalculator.truncate_periods(half_projects, 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 = launch_date + duration + relativedelta(months=total_months) + timedelta(days=total_days-1) prediction = min(prediction, prediction_start + relativedelta(years = 6)) return prediction, considered_full_projects_rounded + considered_half_projects_rounded + non_overlapping_event_periods