summaryrefslogtreecommitdiff
path: root/date_calculator.py
diff options
context:
space:
mode:
authormatin <matin.kaufmann@gmail.com>2025-09-12 20:45:28 +0200
committermatin <matin.kaufmann@gmail.com>2025-09-12 20:45:28 +0200
commit95d784fb414c6270e560fc0cf7ed289765ddd3ab (patch)
tree31f66d2c230634d9325beb82f1125876a3a63e30 /date_calculator.py
parent315bdeffd7b8c7c1a1792cb91d25ff0ac17fecda (diff)
AI refactoring (see architecture analysis and refactoring_summary)
Diffstat (limited to 'date_calculator.py')
-rw-r--r--date_calculator.py129
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