@douglas.onsite.experimentation/douglas-ab-testing-toolkit
Version:
DOUGLAS A/B Testing Toolkit
358 lines (313 loc) • 13 kB
JavaScript
/**
* 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();