diff options
Diffstat (limited to 'date_calculator.py')
| -rw-r--r-- | date_calculator.py | 129 |
1 files changed, 55 insertions, 74 deletions
diff --git a/date_calculator.py b/date_calculator.py index bca7dda..576d6a1 100644 --- a/date_calculator.py +++ b/date_calculator.py @@ -1,50 +1,53 @@ from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta -import math +from typing import List, Tuple class DateCalculator: + """Pure mathematical operations for date and period calculations""" + @staticmethod - def sort_periods(periods): + def sort_periods(periods: List[Tuple[datetime, datetime, str]]) -> List[Tuple[datetime, datetime, str]]: + """Sort periods by start date, then end date""" return sorted(periods, key=lambda p: (p[0], p[1])) @staticmethod - def truncate_periods(periods, launch): + def truncate_periods(periods: List[Tuple[datetime, datetime, str]], launch_date: datetime) -> List[Tuple[datetime, datetime, str]]: + """Truncate periods to start from launch date""" considered_periods = [] - for start, end, id in periods: - # print(start) - # print(launch) - truncated_start = max(start, launch) + for start, end, period_id in periods: + truncated_start = max(start, launch_date) if truncated_start <= end: - considered_periods.append((truncated_start, end, id)) + considered_periods.append((truncated_start, end, period_id)) return considered_periods @staticmethod - def round_periods(periods): + def round_periods(periods: List[Tuple[datetime, datetime, str]]) -> Tuple[List[Tuple[datetime, datetime, str]], int]: + """Round periods to month boundaries and calculate total months""" rounded_periods = [] total_months = 0 - last_end = None - for start, end, id in periods: + for start, end, period_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)) + rounded_periods.append((start, rounded_end, period_id)) total_months += months last_end = rounded_end return rounded_periods, total_months @staticmethod - def adjust_periods(periods): + def adjust_periods(periods: List[Tuple[datetime, datetime, str]]) -> List[Tuple[datetime, datetime, str]]: """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. @@ -53,9 +56,9 @@ class DateCalculator: return [] adjusted = [] - for start, end, pid in periods: + for start, end, period_id in periods: if not adjusted: - adjusted.append((start, end, pid)) + adjusted.append((start, end, period_id)) continue last_start, last_end, last_pid = adjusted[-1] @@ -67,22 +70,23 @@ class DateCalculator: # 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)) + adjusted.append((new_start, end, period_id)) # else new_start > end → discard else: - adjusted.append((start, end, pid)) + adjusted.append((start, end, period_id)) return adjusted @staticmethod - def find_non_overlapping_periods(existing_periods, test_period): - - test_start, test_end, id = test_period + def find_non_overlapping_periods(existing_periods: List[Tuple[datetime, datetime, str]], + test_period: Tuple[datetime, datetime, str]) -> List[Tuple[datetime, datetime, str]]: + """Find non-overlapping parts of a test period against existing periods""" + test_start, test_end, period_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)) + non_overlapping_periods.append((test_start, test_end, period_id)) return non_overlapping_periods elif test_start > end: @@ -90,67 +94,44 @@ class DateCalculator: else: if test_start < start: - non_overlapping_periods.append((test_start, start - timedelta(days=1), id)) + non_overlapping_periods.append((test_start, start - timedelta(days=1), period_id)) test_start = end + timedelta(days=1) if test_start <= test_end: - non_overlapping_periods.append((test_start, test_end, id)) + non_overlapping_periods.append((test_start, test_end, period_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 = {} + def calculate_total_days(periods: List[Tuple[datetime, datetime, str]]) -> int: + """Calculate total days across all periods""" + return sum((end - start).days + 1 for start, end, _ in periods) - 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) - ) + @staticmethod + def calculate_total_months(periods: List[Tuple[datetime, datetime, str]]) -> int: + """Calculate total months across all periods""" + total_months = 0 + for start, end, _ in periods: + 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 + total_months += months + return total_months - 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) + @staticmethod + def add_months_to_date(date: datetime, months: int) -> datetime: + """Add months to a date""" + return date + relativedelta(months=months) - 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)) + @staticmethod + def add_days_to_date(date: datetime, days: int) -> datetime: + """Add days to a date""" + return date + timedelta(days=days) - return prediction, considered_full_projects_rounded + considered_half_projects_rounded + non_overlapping_event_periods
\ No newline at end of file + @staticmethod + def min_date(date1: datetime, date2: datetime) -> datetime: + """Return the minimum of two dates""" + return min(date1, date2)
\ No newline at end of file |
