UNPKG

ts-time-utils

Version:

A comprehensive TypeScript utility library for time, dates, durations, and calendar operations with full tree-shaking support

366 lines (365 loc) 12 kB
/** * Safe JSON date serialization and deserialization utilities */ /** * Safely serialize a date to JSON with various format options */ export function serializeDate(date, options = {}) { const { format = 'iso', includeTimezone = false, useUTC = false, precision = 'milliseconds', customFormat } = options; const dateObj = normalizeDate(date); if (!dateObj) { throw new Error('Invalid date provided for serialization'); } const workingDate = useUTC ? new Date(dateObj.getTime()) : dateObj; switch (format) { case 'iso': return workingDate.toISOString(); case 'epoch': return toEpochTimestamp(workingDate, precision); case 'object': return toDateObject(workingDate, includeTimezone); case 'custom': if (!customFormat) { throw new Error('Custom format string required when format is "custom"'); } return formatCustom(workingDate, customFormat); default: return workingDate.toISOString(); } } /** * Safely deserialize a date from various formats */ export function deserializeDate(serializedDate, options = {}) { const { useUTC = false } = options; try { if (typeof serializedDate === 'string') { return parseISOString(serializedDate, useUTC); } if (typeof serializedDate === 'number') { if (isNaN(serializedDate)) { return null; } return fromEpochTimestamp(serializedDate, options.precision || 'milliseconds'); } if (typeof serializedDate === 'object' && serializedDate !== null) { try { return fromDateObject(serializedDate); } catch { return null; } } return null; } catch (error) { return null; } } /** * Create a safe JSON reviver function for automatic date parsing */ export function createDateReviver(dateKeys = ['createdAt', 'updatedAt', 'date', 'timestamp'], options = {}) { return (key, value) => { if (dateKeys.includes(key) && (typeof value === 'string' || typeof value === 'number')) { const parsed = deserializeDate(value, options); return parsed || value; // Return original value if parsing fails } return value; }; } /** * Create a safe JSON replacer function for automatic date serialization */ export function createDateReplacer(dateKeys = ['createdAt', 'updatedAt', 'date', 'timestamp'], options = {}) { return (key, value) => { if (dateKeys.includes(key)) { if (value instanceof Date) { return serializeDate(value, options); } // Handle case where Date was already converted to ISO string by JSON.stringify if (typeof value === 'string' && isValidISODateString(value)) { const date = parseISOString(value); if (date) { return serializeDate(date, options); } } } return value; }; } /** * Parse ISO string with better error handling */ export function parseISOString(isoString, useUTC = false) { if (!isoString || typeof isoString !== 'string') { return null; } // Handle various ISO formats const cleanedString = isoString.trim(); // Check for valid ISO format const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/; if (!isoRegex.test(cleanedString)) { // Try to parse anyway, but be more forgiving const relaxedRegex = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})?)?$/; if (!relaxedRegex.test(cleanedString)) { return null; } } try { const date = new Date(cleanedString); if (isNaN(date.getTime())) { return null; } return useUTC ? new Date(date.getTime() + (date.getTimezoneOffset() * 60000)) : date; } catch { return null; } } /** * Convert date to epoch timestamp with specified precision */ export function toEpochTimestamp(date, precision = 'milliseconds') { const dateObj = normalizeDate(date); if (!dateObj) { throw new Error('Invalid date provided for epoch conversion'); } const ms = dateObj.getTime(); switch (precision) { case 'seconds': return Math.floor(ms / 1000); case 'microseconds': return ms * 1000; // JavaScript doesn't have true microsecond precision case 'milliseconds': default: return ms; } } /** * Create date from epoch timestamp with specified precision */ export function fromEpochTimestamp(timestamp, precision = 'milliseconds') { let ms; switch (precision) { case 'seconds': ms = timestamp * 1000; break; case 'microseconds': ms = timestamp / 1000; break; case 'milliseconds': default: ms = timestamp; break; } return new Date(ms); } /** * Create epoch timestamp with metadata */ export function createEpochTimestamp(date, precision = 'milliseconds', timezone) { return { timestamp: toEpochTimestamp(date, precision), precision, timezone }; } /** * Convert date to safe object representation */ export function toDateObject(date, includeTimezone = false) { const dateObj = normalizeDate(date); if (!dateObj) { throw new Error('Invalid date provided for object conversion'); } const obj = { year: dateObj.getUTCFullYear(), month: dateObj.getUTCMonth() + 1, // Convert to 1-12 day: dateObj.getUTCDate(), hour: dateObj.getUTCHours(), minute: dateObj.getUTCMinutes(), second: dateObj.getUTCSeconds(), millisecond: dateObj.getUTCMilliseconds() }; if (includeTimezone) { // Get timezone offset in minutes and convert to string format const offset = dateObj.getTimezoneOffset(); const hours = Math.floor(Math.abs(offset) / 60); const minutes = Math.abs(offset) % 60; const sign = offset <= 0 ? '+' : '-'; obj.timezone = `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; } return obj; } /** * Create date from object representation */ export function fromDateObject(dateObj) { // Validate required fields if (!dateObj || typeof dateObj !== 'object') { throw new Error('Invalid date object provided'); } const { year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0 } = dateObj; if (!year || !month || !day) { throw new Error('Date object must include year, month, and day'); } // Validate ranges if (month < 1 || month > 12) { throw new Error('Month must be between 1 and 12'); } if (day < 1 || day > 31) { throw new Error('Day must be between 1 and 31'); } if (hour < 0 || hour > 23) { throw new Error('Hour must be between 0 and 23'); } if (minute < 0 || minute > 59) { throw new Error('Minute must be between 0 and 59'); } if (second < 0 || second > 59) { throw new Error('Second must be between 0 and 59'); } if (millisecond < 0 || millisecond > 999) { throw new Error('Millisecond must be between 0 and 999'); } return new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond)); } /** * Check if a string is a valid ISO date string for serialization */ export function isValidISODateString(dateString) { if (!dateString || typeof dateString !== 'string') { return false; } const parsed = parseISOString(dateString); return parsed !== null; } /** * Check if a number is a valid epoch timestamp */ export function isValidEpochTimestamp(timestamp, precision = 'milliseconds') { if (typeof timestamp !== 'number' || isNaN(timestamp)) { return false; } // Check reasonable bounds for timestamps const now = Date.now(); let min, max; switch (precision) { case 'seconds': min = 0; // Jan 1, 1970 max = Math.floor(now / 1000) + (50 * 365 * 24 * 60 * 60); // 50 years from now break; case 'microseconds': min = 0; max = now * 1000 + (50 * 365 * 24 * 60 * 60 * 1000 * 1000); // 50 years from now break; case 'milliseconds': default: min = 0; // Jan 1, 1970 max = now + (50 * 365 * 24 * 60 * 60 * 1000); // 50 years from now break; } return timestamp >= min && timestamp <= max; } /** * Clone a date safely (avoids reference issues) */ export function cloneDate(date) { const dateObj = normalizeDate(date); return dateObj ? new Date(dateObj.getTime()) : null; } /** * Compare two dates for equality (ignoring milliseconds if specified) */ export function datesEqual(date1, date2, precision = 'milliseconds') { const d1 = normalizeDate(date1); const d2 = normalizeDate(date2); if (!d1 || !d2) { return false; } let time1 = d1.getTime(); let time2 = d2.getTime(); switch (precision) { case 'seconds': time1 = Math.floor(time1 / 1000); time2 = Math.floor(time2 / 1000); break; case 'minutes': time1 = Math.floor(time1 / 60000); time2 = Math.floor(time2 / 60000); break; } return time1 === time2; } /** * Get current timestamp in various formats */ export function now(format = 'date') { const current = new Date(); switch (format) { case 'iso': return current.toISOString(); case 'epoch-ms': return current.getTime(); case 'epoch-s': return Math.floor(current.getTime() / 1000); case 'date': default: return current; } } /** * Safely handle JSON parsing with date conversion */ export function parseJSONWithDates(jsonString, dateKeys, options) { try { return JSON.parse(jsonString, createDateReviver(dateKeys, options)); } catch (error) { throw new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Safely handle JSON stringification with date conversion */ export function stringifyWithDates(obj, dateKeys, options, space) { try { return JSON.stringify(obj, createDateReplacer(dateKeys, options), space); } catch (error) { throw new Error(`Failed to stringify JSON: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Helper functions function normalizeDate(date) { if (date instanceof Date) { return isNaN(date.getTime()) ? null : date; } if (typeof date === 'string') { const parsed = new Date(date); return isNaN(parsed.getTime()) ? null : parsed; } if (typeof date === 'number') { const parsed = new Date(date); return isNaN(parsed.getTime()) ? null : parsed; } return null; } function formatCustom(date, format) { // Simple custom formatter - can be extended const formatMap = { 'YYYY': date.getUTCFullYear().toString(), 'MM': (date.getUTCMonth() + 1).toString().padStart(2, '0'), 'DD': date.getUTCDate().toString().padStart(2, '0'), 'HH': date.getUTCHours().toString().padStart(2, '0'), 'mm': date.getUTCMinutes().toString().padStart(2, '0'), 'ss': date.getUTCSeconds().toString().padStart(2, '0'), 'SSS': date.getUTCMilliseconds().toString().padStart(3, '0') }; let result = format; for (const [token, value] of Object.entries(formatMap)) { result = result.replace(new RegExp(token, 'g'), value); } return result; }