/** * DateRange Value Object * * Represents a validated date range with start and end dates. * Value objects are immutable and enforce validation rules. * * Domain invariants enforced: * - Start date must be before or equal to end date * - Both dates must be valid * * @layer Domain */ /** * DateRange Value Object * * Immutable representation of a date range. */ export class DateRange { private readonly startDate: Date; private readonly endDate: Date; private constructor(startDate: Date, endDate: Date) { this.startDate = new Date(startDate); this.endDate = new Date(endDate); this.validate(); } /** * Creates a new DateRange instance * * @param startDate - Range start date * @param endDate - Range end date * @returns DateRange value object * @throws Error if range is invalid */ static create(startDate: Date | string, endDate: Date | string): DateRange { const start = typeof startDate === 'string' ? new Date(startDate) : startDate; const end = typeof endDate === 'string' ? new Date(endDate) : endDate; return new DateRange(start, end); } /** * Creates a DateRange for the last N days * * @param days - Number of days * @returns DateRange value object */ static lastNDays(days: number): DateRange { const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - days); return new DateRange(startDate, endDate); } /** * Creates a DateRange for the last N months * * @param months - Number of months * @returns DateRange value object */ static lastNMonths(months: number): DateRange { const endDate = new Date(); const startDate = new Date(); startDate.setMonth(startDate.getMonth() - months); return new DateRange(startDate, endDate); } /** * Creates a DateRange for this month * * @returns DateRange value object */ static thisMonth(): DateRange { const now = new Date(); const startDate = new Date(now.getFullYear(), now.getMonth(), 1); const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0); return new DateRange(startDate, endDate); } /** * Creates a DateRange for this year * * @returns DateRange value object */ static thisYear(): DateRange { const now = new Date(); const startDate = new Date(now.getFullYear(), 0, 1); const endDate = new Date(now.getFullYear(), 11, 31); return new DateRange(startDate, endDate); } /** * Validates date range * * @throws Error if range is invalid */ private validate(): void { if (isNaN(this.startDate.getTime())) { throw new Error('Invalid start date'); } if (isNaN(this.endDate.getTime())) { throw new Error('Invalid end date'); } if (this.startDate > this.endDate) { throw new Error('Start date must be before or equal to end date'); } } /** * Gets start date * * @returns Start date (copy) */ getStartDate(): Date { return new Date(this.startDate); } /** * Gets end date * * @returns End date (copy) */ getEndDate(): Date { return new Date(this.endDate); } /** * Gets duration in milliseconds * * @returns Duration in milliseconds */ getDurationMs(): number { return this.endDate.getTime() - this.startDate.getTime(); } /** * Gets duration in days * * @returns Duration in days (rounded) */ getDurationDays(): number { return Math.ceil(this.getDurationMs() / (1000 * 60 * 60 * 24)); } /** * Checks if a date is within this range * * @param date - Date to check * @returns true if date is within range (inclusive) */ contains(date: Date): boolean { return date >= this.startDate && date <= this.endDate; } /** * Checks if this range overlaps with another range * * @param other - DateRange to check * @returns true if ranges overlap */ overlaps(other: DateRange): boolean { return this.startDate <= other.endDate && this.endDate >= other.startDate; } /** * Checks if this range is completely within another range * * @param other - DateRange to check * @returns true if this range is within other */ isWithin(other: DateRange): boolean { return this.startDate >= other.startDate && this.endDate <= other.endDate; } /** * Extends range by adding days * * @param days - Number of days to add to end date * @returns New DateRange with extended end date */ extend(days: number): DateRange { const newEndDate = new Date(this.endDate); newEndDate.setDate(newEndDate.getDate() + days); return new DateRange(this.startDate, newEndDate); } /** * Shifts range forward or backward by days * * @param days - Number of days to shift (positive = forward, negative = backward) * @returns New DateRange shifted by days */ shift(days: number): DateRange { const newStartDate = new Date(this.startDate); newStartDate.setDate(newStartDate.getDate() + days); const newEndDate = new Date(this.endDate); newEndDate.setDate(newEndDate.getDate() + days); return new DateRange(newStartDate, newEndDate); } /** * Checks equality with another DateRange * * @param other - DateRange to compare * @returns true if ranges are equal */ equals(other: DateRange): boolean { return ( this.startDate.getTime() === other.startDate.getTime() && this.endDate.getTime() === other.endDate.getTime() ); } /** * Converts to string representation * * @returns String representation of date range */ toString(): string { return `${this.startDate.toISOString()} - ${this.endDate.toISOString()}`; } /** * Converts to JSON * * @returns Object with start and end dates */ toJSON(): { startDate: string; endDate: string } { return { startDate: this.startDate.toISOString(), endDate: this.endDate.toISOString(), }; } }