"""
Temporal planning utilities for coach-ai
Handles timeframe relevance checking, sprint calculations, and temporal logic.
"""
from datetime import datetime, timedelta
from typing import Optional
def is_timeframe_relevant(
timeframe: Optional[str],
current_date: datetime,
sprint_end_date: Optional[datetime] = None,
) -> bool:
"""
Check if a task's timeframe makes it relevant for today's selection
Args:
timeframe: One of 'this_week', 'next_sprint', 'this_month', 'this_quarter', 'someday', None
current_date: Today's date
sprint_end_date: When current sprint ends (for next_sprint calculation)
Returns:
True if task should be included in daily selection
"""
if timeframe is None:
return True # Unassigned tasks are always available
if timeframe == "this_week":
return True # Always relevant
if timeframe == "next_sprint":
if sprint_end_date is None:
return False # Can't determine relevance without sprint date
days_until_sprint_end = (sprint_end_date - current_date).days
# Show next sprint items in last 5 days of current sprint
return days_until_sprint_end <= 5
if timeframe == "this_month":
# Show month items after week 1 of month
day_of_month = current_date.day
return day_of_month >= 8 # After 1st week
if timeframe == "this_quarter":
# Show quarter items in last 4 weeks of quarter
quarter_end = get_quarter_end(current_date)
days_until_quarter_end = (quarter_end - current_date).days
return days_until_quarter_end <= 28 # 4 weeks
if timeframe == "someday":
return False # Never show automatically
return False
def get_quarter_end(date: datetime) -> datetime:
"""Get last day of current quarter"""
month = date.month
if month <= 3:
return datetime(date.year, 3, 31)
elif month <= 6:
return datetime(date.year, 6, 30)
elif month <= 9:
return datetime(date.year, 9, 30)
else:
return datetime(date.year, 12, 31)
def get_week_start(date: datetime) -> datetime:
"""Get Monday of the week containing date"""
days_since_monday = date.weekday() # Monday = 0
week_start = date - timedelta(days=days_since_monday)
# Return with time set to midnight
return datetime(week_start.year, week_start.month, week_start.day)
def get_next_monday(date: datetime = None) -> datetime:
"""Get next Monday from date (or today if date not provided)"""
if date is None:
date = datetime.now()
# If today is Monday, return next Monday (7 days from now)
days_until_monday = (7 - date.weekday()) % 7
if days_until_monday == 0:
days_until_monday = 7
next_monday = date + timedelta(days=days_until_monday)
return datetime(next_monday.year, next_monday.month, next_monday.day)
def get_current_sprint_end(current_date: datetime = None) -> datetime:
"""
Get the end date of current sprint (assumes 2-week sprints ending on Fridays)
This is a simple heuristic. In production, you'd want to:
- Store sprint dates in database
- Configure sprint start/end dates
- Handle holidays and exceptions
For now: Finds next Friday, then checks if it's 0, 1, or 2 weeks away.
If 0-1 weeks, that's current sprint end. If 2+ weeks, use previous Friday.
"""
if current_date is None:
current_date = datetime.now()
# Find next Friday
days_until_friday = (4 - current_date.weekday()) % 7 # Friday = 4
if days_until_friday == 0 and current_date.hour >= 17:
# If it's Friday afternoon, consider sprint already ended
days_until_friday = 7
next_friday = current_date + timedelta(days=days_until_friday)
# If next Friday is more than 10 days away, use the Friday before it
if days_until_friday > 10:
sprint_end = next_friday - timedelta(days=7)
else:
sprint_end = next_friday
return datetime(sprint_end.year, sprint_end.month, sprint_end.day, 17, 0) # 5pm Friday
def get_next_sprint_end(current_date: datetime = None) -> datetime:
"""Get the end date of next sprint"""
current_sprint_end = get_current_sprint_end(current_date)
return current_sprint_end + timedelta(days=14) # 2 weeks later
def suggest_timeframe_from_keywords(text: str, priority: str) -> Optional[str]:
"""
Suggest timeframe based on keywords and priority
Args:
text: Task title and notes combined
priority: Task priority (high, medium, low)
Returns:
Suggested timeframe or None
"""
import re
text_lower = text.lower()
# Explicit timeframe markers
if re.search(r"\bthis\s+week\b", text_lower):
return "this_week"
if re.search(r"\bnext\s+sprint\b", text_lower):
return "next_sprint"
if re.search(r"\bthis\s+month\b", text_lower):
return "this_month"
if re.search(r"\bthis\s+quarter\b|q[1-4]\b", text_lower):
return "this_quarter"
if re.search(r"\bsomeday\b|\blater\b|\bbacklog\b", text_lower):
return "someday"
# Urgency markers
if re.search(r"\burgent\b|\basap\b|!!|=%", text_lower):
return "this_week"
# Deadline markers (simple date patterns)
if re.search(r"due\s+(?:\d{1,2}[/-]\d{1,2}|\d{1,2}/\d{1,2}/\d{2,4})", text_lower):
# For simplicity, assume deadlines are this week
return "this_week"
# Sprint work keywords
sprint_keywords = ["sprint", "deployment", "deploy", "smoke test", "endpoint", "bug fix", "hotfix"]
if any(kw in text_lower for kw in sprint_keywords):
if priority == "high":
return "this_week"
else:
return "next_sprint"
# Strategic keywords
strategic_keywords = ["pitch", "deck", "platform", "strategy", "planning", "roadmap", "architecture"]
if any(kw in text_lower for kw in strategic_keywords):
if priority == "high":
return "this_month"
else:
return "this_quarter"
# Learning keywords
learning_keywords = ["learn", "explore", "research", "investigate", "experiment", "study"]
if any(kw in text_lower for kw in learning_keywords) and priority == "low":
return "someday"
# Fallback based on priority
if priority == "high":
return "this_week"
elif priority == "medium":
return "next_sprint"
else:
return "this_month"
def days_until_sprint_end(current_date: datetime = None) -> int:
"""Get number of days until current sprint ends"""
if current_date is None:
current_date = datetime.now()
sprint_end = get_current_sprint_end(current_date)
return (sprint_end - current_date).days
def get_week_of_month(date: datetime) -> int:
"""Get which week of the month (1-5) the date falls in"""
# Week 1 = days 1-7, Week 2 = days 8-14, etc.
return ((date.day - 1) // 7) + 1
def get_weeks_left_in_quarter(date: datetime) -> int:
"""Get number of weeks remaining in current quarter"""
quarter_end = get_quarter_end(date)
days_left = (quarter_end - date).days
return days_left // 7