418 lines
No EOL
14 KiB
JavaScript
418 lines
No EOL
14 KiB
JavaScript
/**
|
|
* 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
|