date-range.tsโข4.65 kB
/**
* Date Range Utility
*
* Handles date validation and conversion for RS.ge API
*
* ๐ TEACHING: Why a dedicated date utility?
* - Encapsulates complex date logic in one place
* - Makes the +1 day requirement explicit and documented
* - Prevents date-related bugs by centralizing validation
* - Makes testing easier
*/
import { ValidationError } from './error-handler.js';
/**
* Date range with validation and API format conversion
*/
export class DateRange {
private readonly _startDate: string;
private readonly _endDate: string;
/**
* Create a date range
*
* @param startDate Start date in YYYY-MM-DD format
* @param endDate End date in YYYY-MM-DD format
* @throws ValidationError if dates are invalid
*/
constructor(startDate: string, endDate: string) {
this._startDate = startDate;
this._endDate = endDate;
this.validate();
}
/**
* Get start date in YYYY-MM-DD format
*/
get startDate(): string {
return this._startDate;
}
/**
* Get end date in YYYY-MM-DD format
*/
get endDate(): string {
return this._endDate;
}
/**
* Validate date range
*
* Checks:
* - Format is YYYY-MM-DD
* - Dates are valid calendar dates
* - Start date is not after end date
* - Range is not too large (RS.ge has limits)
*/
private validate(): void {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
// Check format
if (!dateRegex.test(this._startDate)) {
throw new ValidationError(
`Invalid start date format: "${this._startDate}". Expected YYYY-MM-DD (e.g., "2025-10-19")`
);
}
if (!dateRegex.test(this._endDate)) {
throw new ValidationError(
`Invalid end date format: "${this._endDate}". Expected YYYY-MM-DD (e.g., "2025-10-21")`
);
}
// Parse dates
const start = new Date(this._startDate);
const end = new Date(this._endDate);
// Check if dates are valid
if (isNaN(start.getTime())) {
throw new ValidationError(
`Invalid start date: "${this._startDate}" is not a valid calendar date`
);
}
if (isNaN(end.getTime())) {
throw new ValidationError(
`Invalid end date: "${this._endDate}" is not a valid calendar date`
);
}
// Check if start is after end
if (start > end) {
throw new ValidationError(
`Invalid date range: start date (${this._startDate}) is after end date (${this._endDate})`
);
}
// Check if range is too large (RS.ge typically has limits)
const diffDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
const maxDays = 365; // Maximum 1 year range
if (diffDays > maxDays) {
throw new ValidationError(
`Date range too large: ${diffDays} days. Maximum allowed is ${maxDays} days. ` +
`Please split your query into smaller date ranges.`
);
}
}
/**
* Convert to RS.ge API format
*
* โ ๏ธ CRITICAL: RS.ge requires:
* 1. ISO datetime format (YYYY-MM-DDTHH:MM:SS)
* 2. End date must be +1 day to include the full day
*
* Example:
* - Input: 2025-10-19 to 2025-10-21
* - Output: { start: "2025-10-19T00:00:00", end: "2025-10-22T00:00:00" }
*
* @returns Object with start and end in ISO datetime format
*/
toApiFormat(): { start: string; end: string } {
// Start date at 00:00:00
const start = `${this._startDate}T00:00:00`;
// End date +1 day at 00:00:00 (to include full end date)
const endDateObj = new Date(this._endDate);
endDateObj.setDate(endDateObj.getDate() + 1);
const end = endDateObj.toISOString().slice(0, 19);
return { start, end };
}
/**
* Get number of days in range (inclusive)
*/
getDays(): number {
const start = new Date(this._startDate);
const end = new Date(this._endDate);
return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
}
/**
* Check if a date (YYYY-MM-DD or ISO datetime) is within range
*/
contains(dateStr: string): boolean {
// Extract date part (YYYY-MM-DD) from ISO datetime if needed
const datePart = dateStr.substring(0, 10);
return datePart >= this._startDate && datePart <= this._endDate;
}
/**
* Create DateRange from strings with validation
*/
static create(startDate: string, endDate: string): DateRange {
return new DateRange(startDate, endDate);
}
/**
* Format as human-readable string
*/
toString(): string {
const days = this.getDays();
if (days === 1) {
return this._startDate;
}
return `${this._startDate} to ${this._endDate} (${days} days)`;
}
}