Skip to content

Commit ac16da8

Browse files
ljadachclaude
andcommitted
Fix timezone handling in task date analysis
Date comparisons for overdue/due-today detection were using UTC instead of the user's timezone, causing tasks due today to appear overdue and tasks due tomorrow to appear as due today for users in non-UTC timezones. Now fetches user's timezone from RTM settings and converts dates to the user's local timezone before comparison. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 585d8c5 commit ac16da8

2 files changed

Lines changed: 110 additions & 7 deletions

File tree

src/rtm_mcp/tools/tasks.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,20 @@ async def list_tasks(
7676
if not include_completed:
7777
tasks = [t for t in tasks if not t.get("completed")]
7878

79+
# Get user's timezone for accurate date analysis
80+
timezone = None
81+
try:
82+
settings_result = await client.call("rtm.settings.getList")
83+
timezone = settings_result.get("settings", {}).get("timezone")
84+
except Exception:
85+
pass
86+
7987
return build_response(
8088
data={
8189
"tasks": [format_task(t) for t in tasks],
8290
"count": len(tasks),
8391
},
84-
analysis=_analyze_tasks(tasks) if tasks else None,
92+
analysis=_analyze_tasks(tasks, timezone=timezone) if tasks else None,
8593
)
8694

8795
@mcp.tool()
@@ -887,8 +895,14 @@ async def _resolve_task_ids(
887895
}
888896

889897

890-
def _analyze_tasks(tasks: list[dict[str, Any]]) -> dict[str, Any]:
891-
"""Generate analysis insights for tasks."""
898+
def _analyze_tasks(tasks: list[dict[str, Any]], timezone: str | None = None) -> dict[str, Any]:
899+
"""Generate analysis insights for tasks.
900+
901+
Args:
902+
tasks: List of task dictionaries
903+
timezone: User's IANA timezone (e.g., 'Europe/Warsaw'). If not provided,
904+
falls back to UTC which may cause incorrect date comparisons.
905+
"""
892906
if not tasks:
893907
return {}
894908

@@ -898,8 +912,21 @@ def _analyze_tasks(tasks: list[dict[str, Any]]) -> dict[str, Any]:
898912
tags_used: set[str] = set()
899913

900914
from datetime import UTC, datetime
901-
902-
now = datetime.now(UTC)
915+
from zoneinfo import ZoneInfo
916+
917+
# Get current date in user's timezone for accurate comparison
918+
# RTM due dates are relative to the user's timezone
919+
user_tz = None
920+
if timezone:
921+
try:
922+
user_tz = ZoneInfo(timezone)
923+
except Exception:
924+
pass
925+
926+
if user_tz:
927+
now = datetime.now(user_tz)
928+
else:
929+
now = datetime.now(UTC)
903930
today = now.date()
904931

905932
for task in tasks:
@@ -918,10 +945,18 @@ def _analyze_tasks(tasks: list[dict[str, Any]]) -> dict[str, Any]:
918945
due = task.get("due")
919946
if due:
920947
try:
948+
# Parse the due date from RTM
949+
# RTM returns dates in UTC with 'Z' suffix
921950
due_dt = datetime.fromisoformat(due.replace("Z", "+00:00"))
922-
if due_dt.date() < today:
951+
952+
# Convert to user's timezone for comparison
953+
if user_tz:
954+
due_dt = due_dt.astimezone(user_tz)
955+
956+
due_date = due_dt.date()
957+
if due_date < today:
923958
overdue_count += 1
924-
elif due_dt.date() == today:
959+
elif due_date == today:
925960
due_today_count += 1
926961
except ValueError:
927962
pass

tests/test_tools/test_tasks.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,71 @@ def test_analyze_overdue_tasks(self) -> None:
4444
result = _analyze_tasks(tasks)
4545

4646
assert result["summary"]["overdue"] == 1
47+
48+
def test_analyze_tasks_with_timezone(self) -> None:
49+
"""Test that timezone is properly applied for date comparisons."""
50+
from datetime import UTC, datetime, timedelta
51+
from zoneinfo import ZoneInfo
52+
53+
from rtm_mcp.tools.tasks import _analyze_tasks
54+
55+
# Create a task due "today" in a specific timezone
56+
# Use a timezone that's ahead of UTC (e.g., Europe/Warsaw = UTC+1 or UTC+2)
57+
test_tz = ZoneInfo("Europe/Warsaw")
58+
now_local = datetime.now(test_tz)
59+
today_local = now_local.date()
60+
61+
# Create a due date at midnight local time, converted to UTC
62+
due_midnight_local = datetime(
63+
today_local.year, today_local.month, today_local.day, 0, 0, 0, tzinfo=test_tz
64+
)
65+
due_utc = due_midnight_local.astimezone(UTC)
66+
67+
tasks = [
68+
{
69+
"priority": "N",
70+
"due": due_utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
71+
"tags": [],
72+
},
73+
]
74+
75+
# With correct timezone, task should be "due today"
76+
result = _analyze_tasks(tasks, timezone="Europe/Warsaw")
77+
assert result["summary"]["due_today"] == 1
78+
assert result["summary"]["overdue"] == 0
79+
80+
def test_analyze_tasks_timezone_overdue(self) -> None:
81+
"""Test that overdue detection works correctly with timezone."""
82+
from datetime import datetime, timedelta
83+
from zoneinfo import ZoneInfo
84+
85+
from rtm_mcp.tools.tasks import _analyze_tasks
86+
87+
# Create a task that was due yesterday in the user's timezone
88+
test_tz = ZoneInfo("Europe/Warsaw")
89+
now_local = datetime.now(test_tz)
90+
yesterday_local = (now_local - timedelta(days=1)).date()
91+
92+
# Due at noon yesterday local time
93+
due_yesterday = datetime(
94+
yesterday_local.year,
95+
yesterday_local.month,
96+
yesterday_local.day,
97+
12,
98+
0,
99+
0,
100+
tzinfo=test_tz,
101+
)
102+
due_utc = due_yesterday.astimezone(ZoneInfo("UTC"))
103+
104+
tasks = [
105+
{
106+
"priority": "N",
107+
"due": due_utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
108+
"tags": [],
109+
},
110+
]
111+
112+
result = _analyze_tasks(tasks, timezone="Europe/Warsaw")
113+
assert result["summary"]["overdue"] == 1
114+
assert result["summary"]["due_today"] == 0

0 commit comments

Comments
 (0)