|
@@ -0,0 +1,492 @@
|
|
|
+/**
|
|
|
+ * Date utility functions to replace moment.js with native JavaScript Date
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * Format a date to YYYY-MM-DD HH:mm format
|
|
|
+ * @param {Date|string} date - Date to format
|
|
|
+ * @returns {string} Formatted date string
|
|
|
+ */
|
|
|
+export function formatDateTime(date) {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return '';
|
|
|
+
|
|
|
+ const year = d.getFullYear();
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(d.getDate()).padStart(2, '0');
|
|
|
+ const hours = String(d.getHours()).padStart(2, '0');
|
|
|
+ const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
|
+
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Format a date to YYYY-MM-DD format
|
|
|
+ * @param {Date|string} date - Date to format
|
|
|
+ * @returns {string} Formatted date string
|
|
|
+ */
|
|
|
+export function formatDate(date) {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return '';
|
|
|
+
|
|
|
+ const year = d.getFullYear();
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(d.getDate()).padStart(2, '0');
|
|
|
+
|
|
|
+ return `${year}-${month}-${day}`;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Format a time to HH:mm format
|
|
|
+ * @param {Date|string} date - Date to format
|
|
|
+ * @returns {string} Formatted time string
|
|
|
+ */
|
|
|
+export function formatTime(date) {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return '';
|
|
|
+
|
|
|
+ const hours = String(d.getHours()).padStart(2, '0');
|
|
|
+ const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
|
+
|
|
|
+ return `${hours}:${minutes}`;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get ISO week number (ISO 8601)
|
|
|
+ * @param {Date|string} date - Date to get week number for
|
|
|
+ * @returns {number} ISO week number
|
|
|
+ */
|
|
|
+export function getISOWeek(date) {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return 0;
|
|
|
+
|
|
|
+ // Set to nearest Thursday: current date + 4 - current day number
|
|
|
+ // Make Sunday's day number 7
|
|
|
+ const target = new Date(d);
|
|
|
+ const dayNr = (d.getDay() + 6) % 7;
|
|
|
+ target.setDate(target.getDate() - dayNr + 3);
|
|
|
+
|
|
|
+ // ISO week date weeks start on monday, so correct the day number
|
|
|
+ const firstThursday = target.valueOf();
|
|
|
+ target.setMonth(0, 1);
|
|
|
+ if (target.getDay() !== 4) {
|
|
|
+ target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
|
|
|
+ }
|
|
|
+
|
|
|
+ return 1 + Math.ceil((firstThursday - target) / 604800000); // 604800000 = 7 * 24 * 3600 * 1000
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Check if a date is valid
|
|
|
+ * @param {Date|string} date - Date to check
|
|
|
+ * @returns {boolean} True if date is valid
|
|
|
+ */
|
|
|
+export function isValidDate(date) {
|
|
|
+ const d = new Date(date);
|
|
|
+ return !isNaN(d.getTime());
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Check if a date is before another date
|
|
|
+ * @param {Date|string} date1 - First date
|
|
|
+ * @param {Date|string} date2 - Second date
|
|
|
+ * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
|
|
|
+ * @returns {boolean} True if date1 is before date2
|
|
|
+ */
|
|
|
+export function isBefore(date1, date2, unit = 'millisecond') {
|
|
|
+ const d1 = new Date(date1);
|
|
|
+ const d2 = new Date(date2);
|
|
|
+
|
|
|
+ if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
|
|
|
+
|
|
|
+ switch (unit) {
|
|
|
+ case 'year':
|
|
|
+ return d1.getFullYear() < d2.getFullYear();
|
|
|
+ case 'month':
|
|
|
+ return d1.getFullYear() < d2.getFullYear() ||
|
|
|
+ (d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth());
|
|
|
+ case 'day':
|
|
|
+ return d1.getFullYear() < d2.getFullYear() ||
|
|
|
+ (d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth()) ||
|
|
|
+ (d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() < d2.getDate());
|
|
|
+ case 'hour':
|
|
|
+ return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60 * 60)) < Math.floor(d2.getTime() / (1000 * 60 * 60));
|
|
|
+ case 'minute':
|
|
|
+ return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60)) < Math.floor(d2.getTime() / (1000 * 60));
|
|
|
+ default:
|
|
|
+ return d1.getTime() < d2.getTime();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Check if a date is after another date
|
|
|
+ * @param {Date|string} date1 - First date
|
|
|
+ * @param {Date|string} date2 - Second date
|
|
|
+ * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
|
|
|
+ * @returns {boolean} True if date1 is after date2
|
|
|
+ */
|
|
|
+export function isAfter(date1, date2, unit = 'millisecond') {
|
|
|
+ return isBefore(date2, date1, unit);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Check if a date is the same as another date
|
|
|
+ * @param {Date|string} date1 - First date
|
|
|
+ * @param {Date|string} date2 - Second date
|
|
|
+ * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
|
|
|
+ * @returns {boolean} True if dates are the same
|
|
|
+ */
|
|
|
+export function isSame(date1, date2, unit = 'millisecond') {
|
|
|
+ const d1 = new Date(date1);
|
|
|
+ const d2 = new Date(date2);
|
|
|
+
|
|
|
+ if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
|
|
|
+
|
|
|
+ switch (unit) {
|
|
|
+ case 'year':
|
|
|
+ return d1.getFullYear() === d2.getFullYear();
|
|
|
+ case 'month':
|
|
|
+ return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
|
|
|
+ case 'day':
|
|
|
+ return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
|
|
|
+ case 'hour':
|
|
|
+ return Math.floor(d1.getTime() / (1000 * 60 * 60)) === Math.floor(d2.getTime() / (1000 * 60 * 60));
|
|
|
+ case 'minute':
|
|
|
+ return Math.floor(d1.getTime() / (1000 * 60)) === Math.floor(d2.getTime() / (1000 * 60));
|
|
|
+ default:
|
|
|
+ return d1.getTime() === d2.getTime();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Add time to a date
|
|
|
+ * @param {Date|string} date - Base date
|
|
|
+ * @param {number} amount - Amount to add
|
|
|
+ * @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
|
|
|
+ * @returns {Date} New date
|
|
|
+ */
|
|
|
+export function add(date, amount, unit) {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return new Date();
|
|
|
+
|
|
|
+ switch (unit) {
|
|
|
+ case 'years':
|
|
|
+ d.setFullYear(d.getFullYear() + amount);
|
|
|
+ break;
|
|
|
+ case 'months':
|
|
|
+ d.setMonth(d.getMonth() + amount);
|
|
|
+ break;
|
|
|
+ case 'days':
|
|
|
+ d.setDate(d.getDate() + amount);
|
|
|
+ break;
|
|
|
+ case 'hours':
|
|
|
+ d.setHours(d.getHours() + amount);
|
|
|
+ break;
|
|
|
+ case 'minutes':
|
|
|
+ d.setMinutes(d.getMinutes() + amount);
|
|
|
+ break;
|
|
|
+ case 'seconds':
|
|
|
+ d.setSeconds(d.getSeconds() + amount);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ d.setTime(d.getTime() + amount);
|
|
|
+ }
|
|
|
+
|
|
|
+ return d;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Subtract time from a date
|
|
|
+ * @param {Date|string} date - Base date
|
|
|
+ * @param {number} amount - Amount to subtract
|
|
|
+ * @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
|
|
|
+ * @returns {Date} New date
|
|
|
+ */
|
|
|
+export function subtract(date, amount, unit) {
|
|
|
+ return add(date, -amount, unit);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get start of a time unit
|
|
|
+ * @param {Date|string} date - Base date
|
|
|
+ * @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
|
|
|
+ * @returns {Date} Start of unit
|
|
|
+ */
|
|
|
+export function startOf(date, unit) {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return new Date();
|
|
|
+
|
|
|
+ switch (unit) {
|
|
|
+ case 'year':
|
|
|
+ d.setMonth(0, 1);
|
|
|
+ d.setHours(0, 0, 0, 0);
|
|
|
+ break;
|
|
|
+ case 'month':
|
|
|
+ d.setDate(1);
|
|
|
+ d.setHours(0, 0, 0, 0);
|
|
|
+ break;
|
|
|
+ case 'day':
|
|
|
+ d.setHours(0, 0, 0, 0);
|
|
|
+ break;
|
|
|
+ case 'hour':
|
|
|
+ d.setMinutes(0, 0, 0);
|
|
|
+ break;
|
|
|
+ case 'minute':
|
|
|
+ d.setSeconds(0, 0);
|
|
|
+ break;
|
|
|
+ case 'second':
|
|
|
+ d.setMilliseconds(0);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ return d;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get end of a time unit
|
|
|
+ * @param {Date|string} date - Base date
|
|
|
+ * @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
|
|
|
+ * @returns {Date} End of unit
|
|
|
+ */
|
|
|
+export function endOf(date, unit) {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return new Date();
|
|
|
+
|
|
|
+ switch (unit) {
|
|
|
+ case 'year':
|
|
|
+ d.setMonth(11, 31);
|
|
|
+ d.setHours(23, 59, 59, 999);
|
|
|
+ break;
|
|
|
+ case 'month':
|
|
|
+ d.setMonth(d.getMonth() + 1, 0);
|
|
|
+ d.setHours(23, 59, 59, 999);
|
|
|
+ break;
|
|
|
+ case 'day':
|
|
|
+ d.setHours(23, 59, 59, 999);
|
|
|
+ break;
|
|
|
+ case 'hour':
|
|
|
+ d.setMinutes(59, 59, 999);
|
|
|
+ break;
|
|
|
+ case 'minute':
|
|
|
+ d.setSeconds(59, 999);
|
|
|
+ break;
|
|
|
+ case 'second':
|
|
|
+ d.setMilliseconds(999);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ return d;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Format date for display with locale
|
|
|
+ * @param {Date|string} date - Date to format
|
|
|
+ * @param {string} format - Format string (simplified)
|
|
|
+ * @returns {string} Formatted date string
|
|
|
+ */
|
|
|
+export function format(date, format = 'L') {
|
|
|
+ const d = new Date(date);
|
|
|
+ if (isNaN(d.getTime())) return '';
|
|
|
+
|
|
|
+ const year = d.getFullYear();
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(d.getDate()).padStart(2, '0');
|
|
|
+ const hours = String(d.getHours()).padStart(2, '0');
|
|
|
+ const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
|
+ const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
|
+
|
|
|
+ switch (format) {
|
|
|
+ case 'L':
|
|
|
+ return `${month}/${day}/${year}`;
|
|
|
+ case 'LL':
|
|
|
+ return d.toLocaleDateString();
|
|
|
+ case 'LLL':
|
|
|
+ return d.toLocaleString();
|
|
|
+ case 'llll':
|
|
|
+ return d.toLocaleString();
|
|
|
+ case 'YYYY-MM-DD':
|
|
|
+ return `${year}-${month}-${day}`;
|
|
|
+ case 'YYYY-MM-DD HH:mm':
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
|
+ case 'HH:mm':
|
|
|
+ return `${hours}:${minutes}`;
|
|
|
+ default:
|
|
|
+ return d.toLocaleString();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Parse a date string with multiple formats
|
|
|
+ * @param {string} dateString - Date string to parse
|
|
|
+ * @param {string[]} formats - Array of format strings to try
|
|
|
+ * @param {boolean} strict - Whether to use strict parsing
|
|
|
+ * @returns {Date|null} Parsed date or null if invalid
|
|
|
+ */
|
|
|
+export function parseDate(dateString, formats = [], strict = true) {
|
|
|
+ if (!dateString) return null;
|
|
|
+
|
|
|
+ // Try native Date parsing first
|
|
|
+ const nativeDate = new Date(dateString);
|
|
|
+ if (!isNaN(nativeDate.getTime())) {
|
|
|
+ return nativeDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try common formats
|
|
|
+ const commonFormats = [
|
|
|
+ 'YYYY-MM-DD HH:mm',
|
|
|
+ 'YYYY-MM-DD',
|
|
|
+ 'MM/DD/YYYY HH:mm',
|
|
|
+ 'MM/DD/YYYY',
|
|
|
+ 'DD.MM.YYYY HH:mm',
|
|
|
+ 'DD.MM.YYYY',
|
|
|
+ 'DD/MM/YYYY HH:mm',
|
|
|
+ 'DD/MM/YYYY',
|
|
|
+ 'DD-MM-YYYY HH:mm',
|
|
|
+ 'DD-MM-YYYY'
|
|
|
+ ];
|
|
|
+
|
|
|
+ const allFormats = [...formats, ...commonFormats];
|
|
|
+
|
|
|
+ for (const format of allFormats) {
|
|
|
+ const parsed = parseWithFormat(dateString, format);
|
|
|
+ if (parsed && isValidDate(parsed)) {
|
|
|
+ return parsed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Parse date with specific format
|
|
|
+ * @param {string} dateString - Date string to parse
|
|
|
+ * @param {string} format - Format string
|
|
|
+ * @returns {Date|null} Parsed date or null
|
|
|
+ */
|
|
|
+function parseWithFormat(dateString, format) {
|
|
|
+ // Simple format parsing - can be extended as needed
|
|
|
+ const formatMap = {
|
|
|
+ 'YYYY': '\\d{4}',
|
|
|
+ 'MM': '\\d{2}',
|
|
|
+ 'DD': '\\d{2}',
|
|
|
+ 'HH': '\\d{2}',
|
|
|
+ 'mm': '\\d{2}',
|
|
|
+ 'ss': '\\d{2}'
|
|
|
+ };
|
|
|
+
|
|
|
+ let regex = format;
|
|
|
+ for (const [key, value] of Object.entries(formatMap)) {
|
|
|
+ regex = regex.replace(new RegExp(key, 'g'), `(${value})`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const match = dateString.match(new RegExp(regex));
|
|
|
+ if (!match) return null;
|
|
|
+
|
|
|
+ const groups = match.slice(1);
|
|
|
+ let year, month, day, hour = 0, minute = 0, second = 0;
|
|
|
+
|
|
|
+ let groupIndex = 0;
|
|
|
+ for (let i = 0; i < format.length; i++) {
|
|
|
+ if (format[i] === 'Y' && format[i + 1] === 'Y' && format[i + 2] === 'Y' && format[i + 3] === 'Y') {
|
|
|
+ year = parseInt(groups[groupIndex++]);
|
|
|
+ i += 3;
|
|
|
+ } else if (format[i] === 'M' && format[i + 1] === 'M') {
|
|
|
+ month = parseInt(groups[groupIndex++]) - 1;
|
|
|
+ i += 1;
|
|
|
+ } else if (format[i] === 'D' && format[i + 1] === 'D') {
|
|
|
+ day = parseInt(groups[groupIndex++]);
|
|
|
+ i += 1;
|
|
|
+ } else if (format[i] === 'H' && format[i + 1] === 'H') {
|
|
|
+ hour = parseInt(groups[groupIndex++]);
|
|
|
+ i += 1;
|
|
|
+ } else if (format[i] === 'm' && format[i + 1] === 'm') {
|
|
|
+ minute = parseInt(groups[groupIndex++]);
|
|
|
+ i += 1;
|
|
|
+ } else if (format[i] === 's' && format[i + 1] === 's') {
|
|
|
+ second = parseInt(groups[groupIndex++]);
|
|
|
+ i += 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (year === undefined || month === undefined || day === undefined) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return new Date(year, month, day, hour, minute, second);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get current date and time
|
|
|
+ * @returns {Date} Current date
|
|
|
+ */
|
|
|
+export function now() {
|
|
|
+ return new Date();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Create a date from components
|
|
|
+ * @param {number} year - Year
|
|
|
+ * @param {number} month - Month (0-based)
|
|
|
+ * @param {number} day - Day
|
|
|
+ * @param {number} hour - Hour (optional)
|
|
|
+ * @param {number} minute - Minute (optional)
|
|
|
+ * @param {number} second - Second (optional)
|
|
|
+ * @returns {Date} Created date
|
|
|
+ */
|
|
|
+export function createDate(year, month, day, hour = 0, minute = 0, second = 0) {
|
|
|
+ return new Date(year, month, day, hour, minute, second);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get relative time string (e.g., "2 hours ago")
|
|
|
+ * @param {Date|string} date - Date to compare
|
|
|
+ * @param {Date|string} now - Current date (optional)
|
|
|
+ * @returns {string} Relative time string
|
|
|
+ */
|
|
|
+export function fromNow(date, now = new Date()) {
|
|
|
+ const d = new Date(date);
|
|
|
+ const n = new Date(now);
|
|
|
+
|
|
|
+ if (isNaN(d.getTime()) || isNaN(n.getTime())) return '';
|
|
|
+
|
|
|
+ const diffMs = n.getTime() - d.getTime();
|
|
|
+ const diffSeconds = Math.floor(diffMs / 1000);
|
|
|
+ const diffMinutes = Math.floor(diffSeconds / 60);
|
|
|
+ const diffHours = Math.floor(diffMinutes / 60);
|
|
|
+ const diffDays = Math.floor(diffHours / 24);
|
|
|
+ const diffWeeks = Math.floor(diffDays / 7);
|
|
|
+ const diffMonths = Math.floor(diffDays / 30);
|
|
|
+ const diffYears = Math.floor(diffDays / 365);
|
|
|
+
|
|
|
+ if (diffSeconds < 60) return 'a few seconds ago';
|
|
|
+ if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''} ago`;
|
|
|
+ if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
|
|
|
+ if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
|
|
|
+ if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks !== 1 ? 's' : ''} ago`;
|
|
|
+ if (diffMonths < 12) return `${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`;
|
|
|
+ return `${diffYears} year${diffYears !== 1 ? 's' : ''} ago`;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get calendar format (e.g., "Today", "Yesterday", "Tomorrow")
|
|
|
+ * @param {Date|string} date - Date to format
|
|
|
+ * @param {Date|string} now - Current date (optional)
|
|
|
+ * @returns {string} Calendar format string
|
|
|
+ */
|
|
|
+export function calendar(date, now = new Date()) {
|
|
|
+ const d = new Date(date);
|
|
|
+ const n = new Date(now);
|
|
|
+
|
|
|
+ if (isNaN(d.getTime()) || isNaN(n.getTime())) return format(d);
|
|
|
+
|
|
|
+ const diffMs = d.getTime() - n.getTime();
|
|
|
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
+
|
|
|
+ if (diffDays === 0) return 'Today';
|
|
|
+ if (diffDays === 1) return 'Tomorrow';
|
|
|
+ if (diffDays === -1) return 'Yesterday';
|
|
|
+ if (diffDays > 1 && diffDays < 7) return `In ${diffDays} days`;
|
|
|
+ if (diffDays < -1 && diffDays > -7) return `${Math.abs(diffDays)} days ago`;
|
|
|
+
|
|
|
+ return format(d, 'L');
|
|
|
+}
|