This commit introduces the UTC Anchor principle to eliminate timezone-related bugs and provide consistent date handling across all user timezones. ## Key Changes: ### Core Implementation - Add `parseDateToUTC()` function that creates UTC anchors for date-only strings - Add `parseDateToLocal` alias for existing `parseDate` function - Deprecate direct `parseDate` usage with clear migration path - Update date comparison functions to use UTC anchors ### Updated Components - **dateUtils.ts**: New UTC parsing functions and updated comparisons - **main.ts**: Use UTC anchor for selectedDate initialization - **MiniCalendarView**: Navigate using UTC anchors - **TimeblockCreationModal**: Fix timezone handling - **Multiple test files**: Fix timezone assumptions in tests ### Refactored parseDate Usage - **UnscheduledTasksSelectorModal**: Use parseDateToLocal for UI display - **AdvancedCalendarView**: Use parseDateToLocal for calendar events - **helpers.ts**: Use parseDateToUTC for internal date logic - **FilterService**: Already using updated utilities ### Documentation - Add comprehensive UTC Anchor implementation docs - Update development guidelines with date handling best practices - Add migration guide for existing code - Include quick reference for date functions ## Benefits: - Eliminates "off-by-one day" errors across timezones - Provides consistent sorting and filtering behavior - Simplifies date comparison logic - Future-proofs against timezone-related bugs ## Testing: - All 798 tests passing - New UTC anchor test suite (11 tests) - No regression in existing functionality Fixes #327, #322, #314 and other timezone-related issues
12 KiB
UTC-Based Timezone Handling in ChronoSync
Overview
ChronoSync 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
ChronoSync 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:
- Added
formatDateAsUTCString()function for RRule-specific formatting - Updated
isDueByRRuleto useformatDateAsUTCStringinstead offormatDateForStorage - 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:
- Standardized all calendar date formatting to use
formatDateForStorage() - Updated AdvancedCalendarView, TaskEditModal, and other calendar components
- 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:
- Standardized completion date recording to use
formatDateForStorage() - Added
validateCompleteInstances()function to filter invalid time-only entries - Updated all completion workflows to use consistent local date format
Testing Strategy
Timezone-Aware Test Cases
ChronoSync 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:
// Old const dateStr = format(date, 'yyyy-MM-dd'); // New const dateStr = formatDateForStorage(date); -
Update "today" calculations:
// Old const today = new Date(); // New const today = getTodayLocal(); -
Fix date parsing:
// 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 ChronoSync 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.