UTC-Based Timezone Handling in TaskNotes¶
Overview¶
TaskNotes implements a UTC-based timezone approach to ensure consistent date handling across all timezones while maintaining intuitive user behavior. This document explains the technical implementation, design decisions, and best practices for developers working with the codebase.
Core Architecture¶
The UTC Midnight Convention¶
TaskNotes follows a UTC Midnight Convention that operates on two key principles:
- User-facing operations use local dates for intuitive behavior
- Internal calculations use UTC to prevent timezone-dependent bugs
This hybrid approach ensures that: - Users see dates in their local timezone context - The system calculates dates consistently regardless of user timezone - Recurring tasks and date boundaries work correctly across all timezones
Date Flow Architecture¶
User Input (Local) → Storage (YYYY-MM-DD) → Processing (UTC) → Display (Local)
Key Functions and Behavior¶
Date Creation Functions¶
formatDateForStorage(date: Date): string
¶
Purpose: Converts Date objects to YYYY-MM-DD format using UTC components to ensure consistent date representation across timezones.
Behavior:
// Uses UTC methods to prevent timezone shifts
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
When to use: Converting any Date object to storage format, replacing format(date, 'yyyy-MM-dd')
.
getTodayLocal(): Date
¶
Purpose: Returns today's date as a Date object set to midnight local time.
Behavior:
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
When to use: Getting "today" from the user's perspective for comparisons and calculations.
parseDateAsLocal(dateString: string): Date
¶
Purpose: Parses YYYY-MM-DD strings as local dates at midnight to ensure consistent day representation.
Behavior:
// For "2024-10-01", creates Date(2024, 9, 1) - local midnight
const [year, month, day] = dateString.split('-').map(Number);
return new Date(year, month - 1, day);
When to use: Converting date-only strings to Date objects for user-facing operations.
createUTCDateForRRule(dateString: string): Date
¶
Purpose: Creates UTC dates at midnight for RRule operations to preserve correct day-of-week calculations.
Behavior:
// For "2024-10-01", creates Date.UTC(2024, 9, 1) - UTC midnight
return new Date(Date.UTC(year, month - 1, day));
When to use: Converting dates for recurring task calculations with RRule library.
Date Comparison and Utilities¶
isOverdueTimeAware(dateString: string, isCompleted?: boolean, hideCompletedFromOverdue?: boolean): boolean
¶
Purpose: Determines if a date/datetime is overdue with completion status awareness.
Behavior: - For datetime strings: Compares with current moment - For date-only strings: Compares calendar days using local dates - Respects completion status and user preferences
isBeforeDateTimeAware(date1: string, date2: string): boolean
¶
Purpose: Time-aware comparison for sorting tasks with mixed date/datetime formats.
Behavior: - Both have time: Direct comparison - Neither has time: Compare start-of-day - Mixed: Treat date-only as end-of-day for sorting
hasTimeComponent(dateString: string): boolean
¶
Purpose: Detects if a date string includes time information.
Pattern: Checks for 'T' followed by time pattern (HH:mm or HH:mm:ss).
Timezone Handling Guidelines¶
For Developers¶
1. Date Storage and Retrieval¶
// ✅ CORRECT - Use formatDateForStorage for consistent dates
const dueDate = formatDateForStorage(selectedDate);
// ❌ INCORRECT - Don't use date-fns format directly
const dueDate = format(selectedDate, 'yyyy-MM-dd');
2. Getting Today's Date¶
// ✅ CORRECT - Use getTodayLocal for user perspective
const today = getTodayLocal();
// ✅ CORRECT - Use getTodayString for string format
const todayStr = getTodayString();
// ❌ INCORRECT - Don't use new Date() for "today"
const today = new Date(); // Includes time, causes boundary issues
3. Date Parsing¶
// ✅ CORRECT - Use parseDateAsLocal for date-only strings
const taskDate = parseDateAsLocal('2024-10-01');
// ✅ CORRECT - Use parseDate for datetime strings
const timestamp = parseDate('2024-10-01T14:30:00Z');
// ❌ INCORRECT - Don't mix parsing functions
const taskDate = parseDate('2024-10-01'); // May cause timezone shifts
4. Recurring Task Calculations¶
// ✅ CORRECT - Use createUTCDateForRRule for RRule operations
const rruleDate = createUTCDateForRRule(dateString);
const isRecurring = rrule.between(rruleDate, endDate);
// ❌ INCORRECT - Don't use local dates with RRule
const localDate = parseDateAsLocal(dateString);
const isRecurring = rrule.between(localDate, endDate); // Wrong day-of-week
Common Pitfalls and Solutions¶
1. Off-by-One Day Errors¶
Problem: Users in certain timezones see tasks on wrong days.
Cause: Mixing UTC and local date interpretations.
Solution:
// ✅ CORRECT - Consistent local date handling
const dueDate = formatDateForStorage(userSelectedDate);
const taskDate = parseDateAsLocal(dueDate);
const isToday = isSameDay(taskDate, getTodayLocal());
// ❌ INCORRECT - Mixed timezone handling
const dueDate = format(userSelectedDate, 'yyyy-MM-dd'); // UTC
const taskDate = parseDate(dueDate); // Might shift timezone
2. Recurring Task Wrong Day¶
Problem: Recurring tasks appear on incorrect days in some timezones.
Cause: RRule calculations using local dates instead of UTC.
Solution:
// ✅ CORRECT - Use UTC dates for RRule, local for boundaries
const startUTC = createUTCDateForRRule(startDateString);
const checkDateUTC = createUTCDateForRRule(formatDateForStorage(checkDate));
const isRecurring = rrule.between(startUTC, checkDateUTC);
// ❌ INCORRECT - Using local dates with RRule
const startLocal = parseDateAsLocal(startDateString);
const isRecurring = rrule.between(startLocal, checkDate); // Wrong calculations
3. Calendar Display Issues¶
Problem: Tasks show on wrong calendar dates.
Cause: Inconsistent date formatting between storage and display.
Solution:
// ✅ CORRECT - Consistent formatting
const calendarDate = formatDateForStorage(selectedDate);
const tasksForDate = getTasksForDate(calendarDate);
// ❌ INCORRECT - Mixed formatting approaches
const calendarDate = format(selectedDate, 'yyyy-MM-dd'); // Might be UTC
const tasksForDate = getTasksForDate(calendarDate); // Expects local
Recent Fixes (Issues #327, #322, #314)¶
Issue #327: Recurring Task Wrong Day Completion¶
Problem: Users completing recurring tasks on the correct day had the completion recorded for the wrong date, causing the task to still appear as due.
Root Cause: isDueByRRule
function was using formatDateForStorage()
(returns local date) to create dates that were then passed to createUTCDateForRRule()
(expects UTC interpretation), causing a timezone mismatch.
Fix Applied:
1. Added formatDateAsUTCString()
function for RRule-specific formatting
2. Updated isDueByRRule
to use formatDateAsUTCString
instead of formatDateForStorage
3. Ensured all RRule operations use consistent UTC dates
Code Change:
// Before (BROKEN)
const dateStr = formatDateForStorage(targetDate); // Local date string
const rruleDate = createUTCDateForRRule(dateStr); // Interpreted as UTC - MISMATCH!
// After (FIXED)
const dateStr = formatDateAsUTCString(targetDate); // UTC date string
const rruleDate = createUTCDateForRRule(dateStr); // Interpreted as UTC - CONSISTENT!
Issue #322: Calendar Display Timezone Bugs¶
Problem: Tasks appeared on wrong dates in calendar views depending on user timezone.
Root Cause: Inconsistent use of format()
vs formatDateForStorage()
for date string generation.
Fix Applied:
1. Standardized all calendar date formatting to use formatDateForStorage()
2. Updated AdvancedCalendarView, TaskEditModal, and other calendar components
3. Ensured consistent local date interpretation throughout the calendar system
Issue #314: Complete Instances Timezone Inconsistency¶
Problem: Task completion dates stored in complete_instances
array were inconsistent across timezones.
Root Cause: Mixed use of UTC and local date formatting when recording completion dates.
Fix Applied:
1. Standardized completion date recording to use formatDateForStorage()
2. Added validateCompleteInstances()
function to filter invalid time-only entries
3. Updated all completion workflows to use consistent local date format
Testing Strategy¶
Timezone-Aware Test Cases¶
TaskNotes includes comprehensive tests for timezone handling:
- Basic Date Functions: Test date creation, parsing, and formatting across timezones
- Recurring Task Logic: Verify RRule calculations work correctly regardless of user timezone
- Completion Workflows: Test task completion recording and validation
- Calendar Integration: Verify tasks appear on correct dates in all views
- Edge Cases: Test boundary conditions like midnight, DST transitions, etc.
Test Structure Example¶
describe('Timezone handling', () => {
it('should handle recurring tasks consistently across timezones', () => {
// Test setup with specific timezone
const task = createRecurringTask('2024-10-01', 'daily');
const checkDate = new Date('2024-10-05T12:00:00Z');
// Test in different timezone contexts
const isRecurring = isDueByRRule(task, checkDate);
expect(isRecurring).toBe(true); // Should work regardless of test runner timezone
});
});
Migration Notes¶
For Existing Code¶
When updating existing code to follow the UTC-based approach:
- Replace date-fns format calls: ```typescript // Old const dateStr = format(date, 'yyyy-MM-dd');
// New const dateStr = formatDateForStorage(date); ```
- Update "today" calculations: ```typescript // Old const today = new Date();
// New const today = getTodayLocal(); ```
- Fix date parsing: ```typescript // Old (for date-only strings) const date = parseDate('2024-10-01');
// New (for date-only strings) const date = parseDateAsLocal('2024-10-01'); ```
Breaking Changes¶
The UTC-based approach introduces some breaking changes:
- Date Storage Format: All dates now consistently use local timezone interpretation
- RRule Integration: Requires specific UTC handling for recurring task calculations
- API Consistency: Date functions now have clearer, more specific purposes
Performance Considerations¶
The UTC-based approach is designed for performance:
- Reduced Calculations: Fewer timezone conversions in hot paths
- Consistent Caching: Date strings are consistent across operations
- Optimized Comparisons: Time-aware comparisons reduce unnecessary parsing
Future Considerations¶
Potential Enhancements¶
- Timezone-Aware Display: Show tasks with time in user's preferred timezone
- Multi-Timezone Support: Handle tasks created in different timezones
- DST Handling: Enhanced support for daylight saving time transitions
- Performance Optimization: Further caching of timezone calculations
Compatibility¶
The UTC-based approach maintains backward compatibility:
- Existing Data: Works with existing YYYY-MM-DD date formats
- API Stability: Core date functions maintain same signatures
- Plugin Integration: Compatible with Obsidian's date handling expectations
Conclusion¶
The UTC-based timezone approach provides TaskNotes with robust, consistent date handling that prevents timezone-related bugs while maintaining intuitive user behavior. By following the guidelines in this document, developers can ensure their code works correctly for users in all timezones.
For quick reference, see the Timezone Quick Reference Guide for common patterns and functions.