UNPKG

@douglas.onsite.experimentation/douglas-ab-testing-toolkit

Version:

DOUGLAS A/B Testing Toolkit

358 lines (313 loc) 13 kB
/** * DateUtil - A utility class for date manipulation and formatting * @class */ export class DateUtil { /** * Creates a new DateUtil instance * * @param {Date|string|null} date - Initial date (defaults to current date if not provided) * @param {Object} options - Configuration options * @param {string} options.locale - The locale for date formatting (default: 'de-DE') * @param {boolean} options.skipSundays - Whether to skip Sundays when adding days (default: true) * @param {boolean} options.skipSaturdays - Whether to skip Saturdays when adding days (default: false) * @param {boolean} options.businessDaysOnly - Whether to only count business days (Mon-Fri) (default: false) * @param {number} options.cutoffHour - Default cutoff hour for delivery calculations (default: 22) * @param {number} options.cutoffMinute - Default cutoff minute for delivery calculations (default: 0) * @param {number} options.deliveryDaysMax - Maximum number of delivery days (default: 3) * @throws {Error} If date format is invalid or options are invalid */ constructor(date = null, options = {}) { this.#validateOptions(options); this.locale = options.locale || 'de-DE'; this.skipSundays = options.skipSundays !== undefined ? options.skipSundays : true; this.skipSaturdays = options.skipSaturdays || false; this.businessDaysOnly = options.businessDaysOnly || false; this.cutoffHour = this.#validateHour(options.cutoffHour !== undefined ? options.cutoffHour : 22); this.cutoffMinute = this.#validateMinute(options.cutoffMinute || 0); this.deliveryDaysMax = this.#validateDays(options.deliveryDaysMax || 3); this._date = this.#parseDate(date); } /** * Get the current date * * @returns {Date} The current date object */ get date() { return new Date(this._date); } /** * Set the current date * * @param {Date|string} date - The new date * @throws {Error} If date format is invalid */ set date(date) { this._date = this.#parseDate(date); } /** * Format a date according to the specified format options and locale * * @param {Date} date - The date to format * @param {Object} options - Formatting options (same as toLocaleDateString options) * @returns {string} Formatted date string * @throws {Error} If date is invalid */ formatDate(date, options = {}) { if (!(date instanceof Date) || isNaN(date)) { throw new Error('Invalid date provided to formatDate'); } // Ensure consistent formatting with leading zeros const defaultOptions = { day: '2-digit', month: '2-digit', year: 'numeric', ...options }; return date.toLocaleDateString(this.locale, defaultOptions); } /** * Generate a delivery time range message with locale-specific formatting * * @param {Object} options - Configuration options * @param {number} options.startDays - Days to add for start of delivery (default: 1) * @param {number} options.endDays - Days to add for end of delivery (default: 2) * @param {string} options.template - Custom template (default: locale-specific range template) * @returns {string} Formatted delivery range message * @throws {Error} If options are invalid */ getDeliveryRange(options = {}) { this.#validateDeliveryOptions(options); const startDays = options.startDays !== undefined ? options.startDays : this.#getMinDays(); const endDays = options.endDays !== undefined ? options.endDays : this.deliveryDaysMax; const template = options.template; const startDate = this.#getNextValidDay(new Date(), startDays); const endDate = this.#getNextValidDay(startDate, endDays); const startDateUtil = new DateUtil(startDate, { locale: this.locale }); const endDateUtil = new DateUtil(endDate, { locale: this.locale }); const formattedStart = startDateUtil.formatDate(startDate, { day: '2-digit', month: '2-digit', year: 'numeric' }); const formattedEnd = endDateUtil.formatDate(endDate, { day: '2-digit', month: '2-digit', year: 'numeric' }); if (!template) { return `${formattedStart} - ${formattedEnd}`; } return template .replace('{startDate}', formattedStart) .replace('{endDate}', formattedEnd); } /** * Add days to the current date, optionally skipping weekends or counting only business days * * @param {number} days - Number of days to add * @param {Object} options - Options for adding days * @param {boolean} options.skipSundays - Whether to skip Sundays (overrides instance setting) * @param {boolean} options.skipSaturdays - Whether to skip Saturdays (overrides instance setting) * @param {boolean} options.businessDaysOnly - Whether to only count business days (Mon-Fri) * @returns {DateUtil} This instance for method chaining */ addDays(days, options = {}) { if (typeof days !== 'number' || isNaN(days)) { console.log('Invalid number of days provided to addDays'); } const skipSundays = options.skipSundays !== undefined ? options.skipSundays : this.skipSundays; const skipSaturdays = options.skipSaturdays !== undefined ? options.skipSaturdays : this.skipSaturdays; const businessDaysOnly = options.businessDaysOnly !== undefined ? options.businessDaysOnly : this.businessDaysOnly; const newDate = new Date(this._date); let daysToAdd = days; let addedDays = 0; while (addedDays < daysToAdd) { newDate.setDate(newDate.getDate() + 1); const dayOfWeek = newDate.getDay(); if (businessDaysOnly) { // Skip weekends (Saturday = 6, Sunday = 0) if (dayOfWeek !== 0 && dayOfWeek !== 6) { addedDays++; } } else if (skipSundays && dayOfWeek === 0) { // Skip Sunday continue; } else if (skipSaturdays && dayOfWeek === 6) { // Skip Saturday continue; } else { addedDays++; } } this._date = newDate; return this; } /** * Get the day name in the current locale * * @param {string} format - 'short' or 'full' format * @returns {string} Day name */ getDayName(format = 'short') { const options = { weekday: format }; return this._date.toLocaleDateString(this.locale, options); } /** * Format the date with weekday * * @returns {string} Formatted date with weekday */ formatWithWeekday() { const weekday = this.getDayName('short'); const formattedDate = this.formatDate(this._date, { day: '2-digit', month: '2-digit', year: 'numeric' }); return `${weekday}, ${formattedDate}`; } /** * Check if the time is past a cutoff hour/minute (for delivery calculations) * * @param {Object} options - Configuration options * @param {number} options.hour - Hour in 24-hour format (default: instance cutoffHour) * @param {number} options.minute - Minutes (default: instance cutoffMinute) * @returns {boolean} True if current time is past cutoff */ isPastCutoffTime(options = {}) { const cutoffHour = options.hour !== undefined ? options.hour : this.cutoffHour; const cutoffMinute = options.minute !== undefined ? options.minute : this.cutoffMinute; const now = new Date(); const currentHour = now.getHours() + now.getMinutes() / 60; const cutoffDecimal = cutoffHour + (cutoffMinute / 60); return currentHour >= cutoffDecimal; } /** * Get the next valid delivery day by adding days and skipping weekends if needed * * @private * @param {Date} date - The date to start from * @param {number} daysToAdd - Number of days to add * @returns {Date} The next valid delivery date */ #getNextValidDay(date, daysToAdd) { const dateClone = new DateUtil(date, { locale: this.locale, skipSundays: this.skipSundays, skipSaturdays: this.skipSaturdays, businessDaysOnly: this.businessDaysOnly }); return dateClone.addDays(daysToAdd).date; } /** * Calculate the minimum delivery days based on cutoff time * * @private * @returns {number} Minimum delivery days */ #getMinDays() { const now = new Date(); const currentHour = now.getHours() + now.getMinutes() / 60; const cutoffDecimal = this.cutoffHour + (this.cutoffMinute / 60); return currentHour >= cutoffDecimal ? 2 : 1; } /** * Parse and validate a date input * * @private * @param {Date|string|null} date - The date to parse * @returns {Date} Parsed date */ #parseDate(date) { if (date === null) return new Date(); if (date instanceof Date) { if (isNaN(date)) { console.log('Validation Error: Invalid Date object provided'); return new Date(); } return new Date(date); } if (typeof date === 'string') { // Handle DD.MM.YYYY format if (/^\d{2}\.\d{2}\.\d{4}$/.test(date)) { const [day, month, year] = date.split('.').map(Number); return new Date(year, month - 1, day); } const parsedDate = new Date(date); if (isNaN(parsedDate)) { console.log('Validation Error: Invalid date string format'); return new Date(); } return parsedDate; } console.log('Validation Error: Invalid date format'); return new Date(); } /** * Validate options object * * @private * @param {Object} options - Options to validate */ #validateOptions(options) { if (options.locale && typeof options.locale !== 'string') { console.log('Validation Error: Locale must be a string'); } if (options.skipSundays !== undefined && typeof options.skipSundays !== 'boolean') { console.log('Validation Error: skipSundays must be a boolean'); } if (options.skipSaturdays !== undefined && typeof options.skipSaturdays !== 'boolean') { console.log('Validation Error: skipSaturdays must be a boolean'); } if (options.businessDaysOnly !== undefined && typeof options.businessDaysOnly !== 'boolean') { console.log('Validation Error: businessDaysOnly must be a boolean'); } } /** * Validate hour value * * @private * @param {number} hour - Hour to validate * @returns {number} Validated hour */ #validateHour(hour) { if (typeof hour !== 'number' || isNaN(hour) || hour < 0 || hour > 23) { console.log('Validation Error: Hour must be a number between 0 and 23'); } return hour; } /** * Validate minute value * * @private * @param {number} minute - Minute to validate * @returns {number} Validated minute */ #validateMinute(minute) { if (typeof minute !== 'number' || isNaN(minute) || minute < 0 || minute > 59) { console.log('Validation Error: Minute must be a number between 0 and 59'); } return minute; } /** * Validate days value * * @private * @param {number} days - Days to validate * @returns {number} Validated days */ #validateDays(days) { if (typeof days !== 'number' || isNaN(days) || days < 1) { console.log('Validation Error: Days must be a positive number'); } return days; } /** * Validate delivery options * * @private * @param {Object} options - Delivery options to validate */ #validateDeliveryOptions(options) { if (options.startDays !== undefined) { this.#validateDays(options.startDays); } if (options.endDays !== undefined) { this.#validateDays(options.endDays); } if (options.template !== undefined && typeof options.template !== 'string') { console.log('Validation Error: Template must be a string'); } } } // Export a default instance for quick use export default new DateUtil();