This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta
|
||||
import re
|
||||
|
||||
|
||||
def parse_time_to_minutes(value: str) -> int:
|
||||
if not re.fullmatch(r"([01]\d|2[0-3]):[0-5]\d", value):
|
||||
raise ValueError("Uhrzeit muss im Format HH:MM sein")
|
||||
try:
|
||||
parsed = datetime.strptime(value, "%H:%M")
|
||||
except ValueError as exc:
|
||||
raise ValueError("Uhrzeit muss im Format HH:MM sein") from exc
|
||||
return parsed.hour * 60 + parsed.minute
|
||||
|
||||
|
||||
def minutes_to_hhmm(minutes: int) -> str:
|
||||
sign = "-" if minutes < 0 else ""
|
||||
minutes_abs = abs(minutes)
|
||||
hours = minutes_abs // 60
|
||||
mins = minutes_abs % 60
|
||||
return f"{sign}{hours:02d}:{mins:02d}"
|
||||
|
||||
|
||||
def validate_entry(start_minutes: int, end_minutes: int, break_minutes: int) -> None:
|
||||
if end_minutes <= start_minutes:
|
||||
raise ValueError("Arbeitsende muss nach Arbeitsbeginn liegen")
|
||||
|
||||
if break_minutes < 0:
|
||||
raise ValueError("Pause darf nicht negativ sein")
|
||||
|
||||
gross_minutes = end_minutes - start_minutes
|
||||
if break_minutes > gross_minutes:
|
||||
raise ValueError("Pause darf nicht laenger als die Arbeitszeit sein")
|
||||
|
||||
|
||||
def required_break_minutes_for_span(work_span_minutes: int) -> int:
|
||||
if work_span_minutes > 9 * 60:
|
||||
return 45
|
||||
if work_span_minutes > 6 * 60:
|
||||
return 30
|
||||
return 0
|
||||
|
||||
|
||||
def automatic_break_minutes(start_minutes: int, end_minutes: int) -> int:
|
||||
if end_minutes <= start_minutes:
|
||||
raise ValueError("Arbeitsende muss nach Arbeitsbeginn liegen")
|
||||
return required_break_minutes_for_span(end_minutes - start_minutes)
|
||||
|
||||
|
||||
def automatic_break_minutes_for_net_minutes(net_minutes: int) -> int:
|
||||
if net_minutes < 0:
|
||||
raise ValueError("Nettoarbeitszeit darf nicht negativ sein")
|
||||
if net_minutes > (9 * 60 - 45):
|
||||
return 45
|
||||
if net_minutes > (6 * 60 - 30):
|
||||
return 30
|
||||
return 0
|
||||
|
||||
|
||||
def compute_net_minutes(start_minutes: int, end_minutes: int, break_minutes: int) -> int:
|
||||
validate_entry(start_minutes, end_minutes, break_minutes)
|
||||
return (end_minutes - start_minutes) - break_minutes
|
||||
|
||||
|
||||
def iso_week_bounds(day: date) -> tuple[date, date]:
|
||||
week_start = day - timedelta(days=day.weekday())
|
||||
week_end = week_start + timedelta(days=6)
|
||||
return week_start, week_end
|
||||
|
||||
|
||||
def daterange(start: date, end: date):
|
||||
current = start
|
||||
while current <= end:
|
||||
yield current
|
||||
current += timedelta(days=1)
|
||||
|
||||
|
||||
def aggregate_week(entries: list, week_start: date, weekly_target_minutes: int) -> dict:
|
||||
week_end = week_start + timedelta(days=6)
|
||||
entries_by_date = {entry.date: entry for entry in entries}
|
||||
|
||||
days = []
|
||||
weekly_ist = 0
|
||||
for day in daterange(week_start, week_end):
|
||||
entry = entries_by_date.get(day)
|
||||
if entry is None:
|
||||
days.append({"date": day, "entry": None, "net_minutes": 0})
|
||||
continue
|
||||
|
||||
net_minutes = compute_net_minutes(entry.start_minutes, entry.end_minutes, entry.break_minutes)
|
||||
weekly_ist += net_minutes
|
||||
days.append({"date": day, "entry": entry, "net_minutes": net_minutes})
|
||||
|
||||
weekly_delta = weekly_ist - weekly_target_minutes
|
||||
|
||||
return {
|
||||
"week_start": week_start,
|
||||
"week_end": week_end,
|
||||
"days": days,
|
||||
"weekly_ist": weekly_ist,
|
||||
"weekly_soll": weekly_target_minutes,
|
||||
"weekly_delta": weekly_delta,
|
||||
}
|
||||
|
||||
|
||||
def cumulative_delta(entries: list, selected_week_start: date, weekly_target_minutes: int) -> int:
|
||||
if not entries:
|
||||
return 0
|
||||
|
||||
earliest_entry_date = min(entry.date for entry in entries)
|
||||
current_week_start = earliest_entry_date - timedelta(days=earliest_entry_date.weekday())
|
||||
|
||||
net_by_week_start = defaultdict(int)
|
||||
for entry in entries:
|
||||
week_start, _ = iso_week_bounds(entry.date)
|
||||
net_by_week_start[week_start] += compute_net_minutes(
|
||||
entry.start_minutes, entry.end_minutes, entry.break_minutes
|
||||
)
|
||||
|
||||
running = 0
|
||||
while current_week_start <= selected_week_start:
|
||||
weekly_ist = net_by_week_start[current_week_start]
|
||||
running += weekly_ist - weekly_target_minutes
|
||||
current_week_start += timedelta(days=7)
|
||||
|
||||
return running
|
||||
Reference in New Issue
Block a user