/** * DateService - Unified date/time service using day.js * Handles all date operations, timezone conversions, and formatting */ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import isoWeek from 'dayjs/plugin/isoWeek'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; // Enable day.js plugins dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(isoWeek); dayjs.extend(customParseFormat); dayjs.extend(isSameOrAfter); dayjs.extend(isSameOrBefore); export class DateService { constructor(config) { this.timezone = config.timeFormatConfig.timezone; } // ============================================ // CORE CONVERSIONS // ============================================ /** * Convert local date to UTC ISO string * @param localDate - Date in local timezone * @returns ISO string in UTC (with 'Z' suffix) */ toUTC(localDate) { return dayjs.tz(localDate, this.timezone).utc().toISOString(); } /** * Convert UTC ISO string to local date * @param utcString - ISO string in UTC * @returns Date in local timezone */ fromUTC(utcString) { return dayjs.utc(utcString).tz(this.timezone).toDate(); } // ============================================ // FORMATTING // ============================================ /** * Format time as HH:mm or HH:mm:ss * @param date - Date to format * @param showSeconds - Include seconds in output * @returns Formatted time string */ formatTime(date, showSeconds = false) { const pattern = showSeconds ? 'HH:mm:ss' : 'HH:mm'; return dayjs(date).format(pattern); } /** * Format time range as "HH:mm - HH:mm" * @param start - Start date * @param end - End date * @returns Formatted time range */ formatTimeRange(start, end) { return `${this.formatTime(start)} - ${this.formatTime(end)}`; } /** * Format date and time in technical format: yyyy-MM-dd HH:mm:ss * @param date - Date to format * @returns Technical datetime string */ formatTechnicalDateTime(date) { return dayjs(date).format('YYYY-MM-DD HH:mm:ss'); } /** * Format date as yyyy-MM-dd * @param date - Date to format * @returns ISO date string */ formatDate(date) { return dayjs(date).format('YYYY-MM-DD'); } /** * Format date as "Month Year" (e.g., "January 2025") * @param date - Date to format * @param locale - Locale for month name (default: 'en-US') * @returns Formatted month and year */ formatMonthYear(date, locale = 'en-US') { return date.toLocaleDateString(locale, { month: 'long', year: 'numeric' }); } /** * Format date as ISO string (same as formatDate for compatibility) * @param date - Date to format * @returns ISO date string */ formatISODate(date) { return this.formatDate(date); } /** * Format time in 12-hour format with AM/PM * @param date - Date to format * @returns Time string in 12-hour format (e.g., "2:30 PM") */ formatTime12(date) { return dayjs(date).format('h:mm A'); } /** * Get day name for a date * @param date - Date to get day name for * @param format - 'short' (e.g., 'Mon') or 'long' (e.g., 'Monday') * @param locale - Locale for day name (default: 'da-DK') * @returns Day name */ getDayName(date, format = 'short', locale = 'da-DK') { const formatter = new Intl.DateTimeFormat(locale, { weekday: format }); return formatter.format(date); } /** * Format a date range with customizable options * @param start - Start date * @param end - End date * @param options - Formatting options * @returns Formatted date range string */ formatDateRange(start, end, options = {}) { const { locale = 'en-US', month = 'short', day = 'numeric' } = options; const startYear = start.getFullYear(); const endYear = end.getFullYear(); const formatter = new Intl.DateTimeFormat(locale, { month, day, year: startYear !== endYear ? 'numeric' : undefined }); // @ts-ignore - formatRange is available in modern browsers if (typeof formatter.formatRange === 'function') { // @ts-ignore return formatter.formatRange(start, end); } return `${formatter.format(start)} - ${formatter.format(end)}`; } // ============================================ // TIME CALCULATIONS // ============================================ /** * Convert time string (HH:mm or HH:mm:ss) to total minutes since midnight * @param timeString - Time in format HH:mm or HH:mm:ss * @returns Total minutes since midnight */ timeToMinutes(timeString) { const parts = timeString.split(':').map(Number); const hours = parts[0] || 0; const minutes = parts[1] || 0; return hours * 60 + minutes; } /** * Convert total minutes since midnight to time string HH:mm * @param totalMinutes - Minutes since midnight * @returns Time string in format HH:mm */ minutesToTime(totalMinutes) { const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; return dayjs().hour(hours).minute(minutes).format('HH:mm'); } /** * Format time from total minutes (alias for minutesToTime) * @param totalMinutes - Minutes since midnight * @returns Time string in format HH:mm */ formatTimeFromMinutes(totalMinutes) { return this.minutesToTime(totalMinutes); } /** * Get minutes since midnight for a given date * @param date - Date to calculate from * @returns Minutes since midnight */ getMinutesSinceMidnight(date) { const d = dayjs(date); return d.hour() * 60 + d.minute(); } /** * Calculate duration in minutes between two dates * @param start - Start date or ISO string * @param end - End date or ISO string * @returns Duration in minutes */ getDurationMinutes(start, end) { const startDate = dayjs(start); const endDate = dayjs(end); return endDate.diff(startDate, 'minute'); } // ============================================ // WEEK OPERATIONS // ============================================ /** * Get start and end of week (Monday to Sunday) * @param date - Reference date * @returns Object with start and end dates */ getWeekBounds(date) { const d = dayjs(date); return { start: d.startOf('week').add(1, 'day').toDate(), // Monday (day.js week starts on Sunday) end: d.endOf('week').add(1, 'day').toDate() // Sunday }; } /** * Add weeks to a date * @param date - Base date * @param weeks - Number of weeks to add (can be negative) * @returns New date */ addWeeks(date, weeks) { return dayjs(date).add(weeks, 'week').toDate(); } /** * Add months to a date * @param date - Base date * @param months - Number of months to add (can be negative) * @returns New date */ addMonths(date, months) { return dayjs(date).add(months, 'month').toDate(); } /** * Get ISO week number (1-53) * @param date - Date to get week number for * @returns ISO week number */ getWeekNumber(date) { return dayjs(date).isoWeek(); } /** * Get all dates in a full week (7 days starting from given date) * @param weekStart - Start date of the week * @returns Array of 7 dates */ getFullWeekDates(weekStart) { const dates = []; for (let i = 0; i < 7; i++) { dates.push(this.addDays(weekStart, i)); } return dates; } /** * Get dates for work week using ISO 8601 day numbering (Monday=1, Sunday=7) * @param weekStart - Any date in the week * @param workDays - Array of ISO day numbers (1=Monday, 7=Sunday) * @returns Array of dates for the specified work days */ getWorkWeekDates(weekStart, workDays) { const dates = []; // Get Monday of the week const weekBounds = this.getWeekBounds(weekStart); const mondayOfWeek = this.startOfDay(weekBounds.start); // Calculate dates for each work day using ISO numbering workDays.forEach(isoDay => { const date = new Date(mondayOfWeek); // ISO day 1=Monday is +0 days, ISO day 7=Sunday is +6 days const daysFromMonday = isoDay === 7 ? 6 : isoDay - 1; date.setDate(mondayOfWeek.getDate() + daysFromMonday); dates.push(date); }); return dates; } // ============================================ // GRID HELPERS // ============================================ /** * Create a date at a specific time (minutes since midnight) * @param baseDate - Base date (date component) * @param totalMinutes - Minutes since midnight * @returns New date with specified time */ createDateAtTime(baseDate, totalMinutes) { const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; return dayjs(baseDate).startOf('day').hour(hours).minute(minutes).toDate(); } /** * Snap date to nearest interval * @param date - Date to snap * @param intervalMinutes - Snap interval in minutes * @returns Snapped date */ snapToInterval(date, intervalMinutes) { const minutes = this.getMinutesSinceMidnight(date); const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes; return this.createDateAtTime(date, snappedMinutes); } // ============================================ // UTILITY METHODS // ============================================ /** * Check if two dates are the same day * @param date1 - First date * @param date2 - Second date * @returns True if same day */ isSameDay(date1, date2) { return dayjs(date1).isSame(date2, 'day'); } /** * Get start of day * @param date - Date * @returns Start of day (00:00:00) */ startOfDay(date) { return dayjs(date).startOf('day').toDate(); } /** * Get end of day * @param date - Date * @returns End of day (23:59:59.999) */ endOfDay(date) { return dayjs(date).endOf('day').toDate(); } /** * Add days to a date * @param date - Base date * @param days - Number of days to add (can be negative) * @returns New date */ addDays(date, days) { return dayjs(date).add(days, 'day').toDate(); } /** * Add minutes to a date * @param date - Base date * @param minutes - Number of minutes to add (can be negative) * @returns New date */ addMinutes(date, minutes) { return dayjs(date).add(minutes, 'minute').toDate(); } /** * Parse ISO string to date * @param isoString - ISO date string * @returns Parsed date */ parseISO(isoString) { return dayjs(isoString).toDate(); } /** * Check if date is valid * @param date - Date to check * @returns True if valid */ isValid(date) { return dayjs(date).isValid(); } /** * Calculate difference in calendar days between two dates * @param date1 - First date * @param date2 - Second date * @returns Number of calendar days between dates (can be negative) */ differenceInCalendarDays(date1, date2) { const d1 = dayjs(date1).startOf('day'); const d2 = dayjs(date2).startOf('day'); return d1.diff(d2, 'day'); } /** * Validate date range (start must be before or equal to end) * @param start - Start date * @param end - End date * @returns True if valid range */ isValidRange(start, end) { if (!this.isValid(start) || !this.isValid(end)) { return false; } return start.getTime() <= end.getTime(); } /** * Check if date is within reasonable bounds (1900-2100) * @param date - Date to check * @returns True if within bounds */ isWithinBounds(date) { if (!this.isValid(date)) { return false; } const year = date.getFullYear(); return year >= 1900 && year <= 2100; } /** * Validate date with comprehensive checks * @param date - Date to validate * @param options - Validation options * @returns Validation result with error message */ validateDate(date, options = {}) { if (!this.isValid(date)) { return { valid: false, error: 'Invalid date' }; } if (!this.isWithinBounds(date)) { return { valid: false, error: 'Date out of bounds (1900-2100)' }; } const now = new Date(); if (options.requireFuture && date <= now) { return { valid: false, error: 'Date must be in the future' }; } if (options.requirePast && date >= now) { return { valid: false, error: 'Date must be in the past' }; } if (options.minDate && date < options.minDate) { return { valid: false, error: `Date must be after ${this.formatDate(options.minDate)}` }; } if (options.maxDate && date > options.maxDate) { return { valid: false, error: `Date must be before ${this.formatDate(options.maxDate)}` }; } return { valid: true }; } } //# sourceMappingURL=DateService.js.map