-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproject_dates.py
More file actions
59 lines (46 loc) · 1.8 KB
/
project_dates.py
File metadata and controls
59 lines (46 loc) · 1.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from __future__ import annotations
import math
from datetime import date, datetime, time, timedelta, timezone
def parse_iso_date(value: str | None) -> date | None:
if not value:
return None
try:
return datetime.fromisoformat(value).date()
except ValueError:
return None
def _format_hours(delta: timedelta) -> str:
seconds = max(delta.total_seconds(), 0)
hours = math.ceil(seconds / 3600) if seconds else 0
return f"{hours}h"
def format_project_start_status(
start_date: date | None, now: datetime | None = None
) -> tuple[int | None, str | None]:
if start_date is None:
return None, None
current_time = now or datetime.now(timezone.utc)
start_at = datetime.combine(start_date, time.min, tzinfo=timezone.utc)
starts_in = (start_date - current_time.date()).days
if starts_in <= 0:
return starts_in, None
if start_at - current_time < timedelta(days=1):
return starts_in, f"starts in {_format_hours(start_at - current_time)}"
return starts_in, f"starts in {starts_in}d"
def format_project_target_status(
target_date: date | None, now: datetime | None = None
) -> tuple[int | None, str | None]:
if target_date is None:
return None, None
current_time = now or datetime.now(timezone.utc)
deadline_at = datetime.combine(
target_date + timedelta(days=1),
time.min,
tzinfo=timezone.utc,
)
delta = deadline_at - current_time
days_left = (target_date - current_time.date()).days
if abs(delta) < timedelta(days=1):
direction = "left" if delta.total_seconds() >= 0 else "overdue"
return days_left, f"{_format_hours(abs(delta))} {direction}"
if days_left < 0:
return days_left, f"{abs(days_left)}d overdue"
return days_left, f"{days_left}d left"