chore: initialize public repository
CI / checks (push) Has been cancelled

This commit is contained in:
maddin
2026-03-22 12:55:55 +00:00
commit 9794362f39
143 changed files with 19832 additions and 0 deletions
+126
View File
@@ -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