import Foundation /// Utility for formatting dates in a human-readable format /// Mirrors the shared Kotlin DateUtils for consistent date display enum DateUtils { private static let isoDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" return formatter }() private static let isoDateTimeFormatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter }() private static let isoDateTimeSimpleFormatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() return formatter }() private static let mediumDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "MMM d, yyyy" return formatter }() private static let dateTimeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "MMM d, yyyy 'at' h:mm a" return formatter }() /// Format a date string (YYYY-MM-DD) to a human-readable format /// Returns "Today", "Tomorrow", "Yesterday", or "Dec 15, 2024" format static func formatDate(_ dateString: String?) -> String { guard let dateString = dateString, !dateString.isEmpty else { return "" } // Extract date part if it includes time let datePart = dateString.components(separatedBy: "T").first ?? dateString guard let date = isoDateFormatter.date(from: datePart) else { return dateString } let today = Calendar.current.startOfDay(for: Date()) let targetDate = Calendar.current.startOfDay(for: date) let daysDiff = Calendar.current.dateComponents([.day], from: today, to: targetDate).day ?? 0 switch daysDiff { case 0: return "Today" case 1: return "Tomorrow" case -1: return "Yesterday" default: return mediumDateFormatter.string(from: date) } } /// Format a date string to medium format (e.g., "Dec 15, 2024") static func formatDateMedium(_ dateString: String?) -> String { guard let dateString = dateString, !dateString.isEmpty else { return "" } // Extract date part if it includes time let datePart = dateString.components(separatedBy: "T").first ?? dateString guard let date = isoDateFormatter.date(from: datePart) else { return dateString } return mediumDateFormatter.string(from: date) } /// Format an ISO datetime string to a human-readable date format /// Handles formats like "2024-01-01T00:00:00Z" or "2024-01-01T00:00:00.000Z" static func formatDateTime(_ dateTimeString: String?) -> String { guard let dateTimeString = dateTimeString, !dateTimeString.isEmpty else { return "" } // Try parsing with fractional seconds first var date = isoDateTimeFormatter.date(from: dateTimeString) // Fallback to simple ISO format if date == nil { date = isoDateTimeSimpleFormatter.date(from: dateTimeString) } // Fallback to just date parsing guard let parsedDate = date else { return formatDate(dateTimeString) } let today = Calendar.current.startOfDay(for: Date()) let targetDate = Calendar.current.startOfDay(for: parsedDate) let daysDiff = Calendar.current.dateComponents([.day], from: today, to: targetDate).day ?? 0 switch daysDiff { case 0: return "Today" case 1: return "Tomorrow" case -1: return "Yesterday" default: return mediumDateFormatter.string(from: parsedDate) } } /// Format a datetime string with time (e.g., "Dec 15, 2024 at 3:30 PM") static func formatDateTimeWithTime(_ dateTimeString: String?) -> String { guard let dateTimeString = dateTimeString, !dateTimeString.isEmpty else { return "" } // Try parsing with fractional seconds first var date = isoDateTimeFormatter.date(from: dateTimeString) // Fallback to simple ISO format if date == nil { date = isoDateTimeSimpleFormatter.date(from: dateTimeString) } guard let parsedDate = date else { return formatDate(dateTimeString) } return dateTimeFormatter.string(from: parsedDate) } /// Format a date for relative display (e.g., "2 days ago", "in 3 days") static func formatRelativeDate(_ dateString: String?) -> String { guard let dateString = dateString, !dateString.isEmpty else { return "" } // Extract date part if it includes time let datePart = dateString.components(separatedBy: "T").first ?? dateString guard let date = isoDateFormatter.date(from: datePart) else { return dateString } let today = Calendar.current.startOfDay(for: Date()) let targetDate = Calendar.current.startOfDay(for: date) let daysDiff = Calendar.current.dateComponents([.day], from: today, to: targetDate).day ?? 0 switch daysDiff { case 0: return "Today" case 1: return "Tomorrow" case -1: return "Yesterday" case 2...7: return "in \(daysDiff) days" case -7 ... -2: return "\(-daysDiff) days ago" default: return mediumDateFormatter.string(from: date) } } /// Check if a date string represents a date in the past static func isOverdue(_ dateString: String?) -> Bool { guard let dateString = dateString, !dateString.isEmpty else { return false } // Extract date part if it includes time let datePart = dateString.components(separatedBy: "T").first ?? dateString guard let date = isoDateFormatter.date(from: datePart) else { return false } let today = Calendar.current.startOfDay(for: Date()) return date < today } // MARK: - Timezone Conversion Utilities /// Convert a local hour (0-23) to UTC hour /// - Parameter localHour: Hour in the device's local timezone (0-23) /// - Returns: Hour in UTC (0-23) static func localHourToUtc(_ localHour: Int) -> Int { let now = Date() let calendar = Calendar.current // Create a date with the given local hour var components = calendar.dateComponents(in: TimeZone.current, from: now) components.hour = localHour components.minute = 0 components.second = 0 guard let localDate = calendar.date(from: components) else { return localHour } // Get the hour in UTC var utcCalendar = Calendar.current utcCalendar.timeZone = TimeZone(identifier: "UTC")! let utcHour = utcCalendar.component(.hour, from: localDate) return utcHour } /// Convert a UTC hour (0-23) to local hour /// - Parameter utcHour: Hour in UTC (0-23) /// - Returns: Hour in the device's local timezone (0-23) static func utcHourToLocal(_ utcHour: Int) -> Int { let now = Date() // Create a calendar in UTC var utcCalendar = Calendar.current utcCalendar.timeZone = TimeZone(identifier: "UTC")! // Create a date with the given UTC hour var components = utcCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now) components.hour = utcHour components.minute = 0 components.second = 0 guard let utcDate = utcCalendar.date(from: components) else { return utcHour } // Get the hour in local timezone let localHour = Calendar.current.component(.hour, from: utcDate) return localHour } /// Format an hour (0-23) to a human-readable 12-hour format /// - Parameter hour: Hour in 24-hour format (0-23) /// - Returns: Formatted string like "8:00 AM" or "2:00 PM" static func formatHour(_ hour: Int) -> String { switch hour { case 0: return "12:00 AM" case 1..<12: return "\(hour):00 AM" case 12: return "12:00 PM" default: return "\(hour - 12):00 PM" } } }