feat: set up modularized version of project with testing

This commit is contained in:
Jef Roosens 2026-05-22 10:09:59 +02:00
commit 7bea08ddac
Signed by: Jef Roosens
GPG key ID: 119385BCAA005C21
19 changed files with 1138 additions and 0 deletions

44
src/timesheets/utils.py Normal file
View file

@ -0,0 +1,44 @@
import re
from datetime import date
def parse_duration(duration_str: str) -> float:
"""Convert HH:MM duration string to decimal hours."""
duration_str = duration_str.strip()
match = re.match(r"^(\d+):(\d{2})$", duration_str)
if not match:
raise ValueError(f"Invalid duration format: {duration_str!r}")
return int(match.group(1)) + int(match.group(2)) / 60.0
def duration_from_start_end(start_str: str, end_str: str) -> float:
"""Calculate duration in decimal hours from two HH:MM time strings."""
def to_minutes(t: str) -> int:
match = re.match(r"^(\d+):(\d{2})$", t.strip())
if not match:
raise ValueError(f"Invalid time format: {t!r}")
return int(match.group(1)) * 60 + int(match.group(2))
start_minutes = to_minutes(start_str)
end_minutes = to_minutes(end_str)
if end_minutes < start_minutes:
end_minutes += 24 * 60 # midnight rollover
return (end_minutes - start_minutes) / 60.0
def decimal_to_hhmm(hours: float) -> str:
"""Convert decimal hours to a HH:MM string."""
total_minutes = round(hours * 60)
h, m = divmod(total_minutes, 60)
return f"{h:02d}:{m:02d}"
def strip_markdown_link(text: str) -> str:
"""Strip markdown link syntax [label](url), keeping only the label."""
return re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", text)
def format_date(d: date) -> str:
"""Format date as DD/MM/YY."""
return d.strftime("%d/%m/%y")