UNPKG

chronos-ts

Version:

A comprehensive TypeScript package for handling time periods, intervals, and date-related operations.

366 lines (365 loc) 14.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Period = void 0; const precision_1 = require("./precision"); const utils_1 = require("./utils"); class Period { /** * Constructs a new Period instance. * * @param start - The start date of the period, either as a string or a Date object. * @param end - The end date of the period, either as a string or a Date object. * @param precision - The precision level of the period, defaulting to Precision.DAY. * @param interval - An optional interval associated with the period, defaulting to null. * @throws {Error} If start date is after end date or if dates are invalid. */ constructor(start, end, precision = precision_1.Precision.DAY, interval = null) { this.startDate = this.validateDate(start); this.endDate = this.validateDate(end); if (this.startDate > this.endDate) { throw new Error('Start date must be before or equal to end date'); } this.precision = precision; this.interval = interval; } /** * Validates and converts a date input to a Date object. * * @param date - The date to validate, either as a string or Date object. * @returns A valid Date object. * @throws {Error} If the date is invalid. */ validateDate(date) { const result = new Date(date); if (isNaN(result.getTime())) { throw new Error(`Invalid date: ${date}`); } return result; } /** * Retrieves the start date of the period. * * @returns {Date} The start date as a Date object. */ getStartDate() { return new Date(this.startDate); } /** * Retrieves the end date of the period. * * @returns {Date} The end date as a Date object. */ getEndDate() { return new Date(this.endDate); } /** * Checks if a given date is within the period defined by `startDate` and `endDate`. * * @param date - The date to check, either as a string or a Date object. * @returns `true` if the date is within the period, `false` otherwise. */ contains(date) { const checkDate = this.validateDate(date); return checkDate >= this.startDate && checkDate <= this.endDate; } /** * Determines if the current period overlaps with another period. * * @param other - The other period to compare with. * @returns `true` if the periods overlap, otherwise `false`. */ overlapsWith(other) { return this.startDate < other.endDate && this.endDate > other.startDate; } /** * Determines if the current period is adjacent to another period. * * Two periods are considered adjacent if they do not overlap and the gap * between them is within one precision unit of the larger precision of the two periods. * * @param other - The other period to compare with. * @returns `true` if the periods are adjacent, `false` otherwise. */ isAdjacentTo(other) { const thisPrecisionMs = (0, precision_1.getPrecisionInMilliseconds)(this.precision); const otherPrecisionMs = (0, precision_1.getPrecisionInMilliseconds)(other.precision); const maxPrecisionMs = Math.max(thisPrecisionMs, otherPrecisionMs); const thisEndTime = this.endDate.getTime(); const otherStartTime = other.startDate.getTime(); const thisStartTime = this.startDate.getTime(); const otherEndTime = other.endDate.getTime(); // Check for overlap if (thisStartTime <= otherEndTime && otherStartTime <= thisEndTime) { return false; } // Check if the gap between periods is within one precision unit const gap = Math.abs(otherStartTime - thisEndTime); const reverseGap = Math.abs(thisStartTime - otherEndTime); return ((gap > 0 && gap <= maxPrecisionMs) || (reverseGap > 0 && reverseGap <= maxPrecisionMs)); } /** * Retrieves an array of dates within the specified interval. * * @returns {Date[] | null} An array of dates if the interval is defined, otherwise `null`. */ getDatesInInterval() { if (this.interval) { return (0, utils_1.getDatesWithInterval)(this.startDate, this.endDate, this.interval); } return null; } /** * Calculates the number of minutes in the interval between the start and end dates. * * @returns {number} The number of minutes between the start and end dates. */ getMinutesInInterval() { return Math.floor((this.endDate.getTime() - this.startDate.getTime()) / (1000 * 60)); } /** * Calculates the number of whole hours in the interval. * * @returns {number} The number of whole hours in the interval. */ getHoursInInterval() { return Math.floor(this.getMinutesInInterval() / 60); } /** * Calculates the number of whole days in the interval. * * @returns The number of whole days in the interval. */ getDaysInInterval() { return Math.floor(this.getHoursInInterval() / 24); } /** * Calculates the number of whole weeks in the interval. * * @returns The number of whole weeks in the interval. */ getWeeksInInterval() { return Math.floor(this.getDaysInInterval() / 7); } /** * Calculates the number of whole months in the interval between the start and end dates. * * This method computes the difference in months between the `startDate` and `endDate` properties. * It adjusts for month boundaries by decrementing the month count if the end date's day is less than the start date's day. * * @returns {number} The number of whole months in the interval. */ getMonthsInInterval() { let months = (this.endDate.getFullYear() - this.startDate.getFullYear()) * 12; months += this.endDate.getMonth() - this.startDate.getMonth(); // Adjust for month boundaries if (this.endDate.getDate() < this.startDate.getDate()) { months--; } return Math.max(0, months); } /** * Calculates the number of whole years in the interval. * * @returns The number of whole years in the interval. */ getYearsInInterval() { return Math.floor(this.getMonthsInInterval() / 12); } /** * Calculates the length of the period in the specified precision. * * @returns {number} The length of the period, rounded down to the nearest whole number. */ length() { return Math.floor((this.endDate.getTime() - this.startDate.getTime()) / (0, precision_1.getPrecisionInMilliseconds)(this.precision)); } /** * Determines the overlapping period between this period and another period. * * @param other - The other period to check for overlap with. * @returns The overlapping period as a new `Period` instance, or `null` if there is no overlap. */ overlap(other) { if (!this.overlapsWith(other)) { return null; } const start = new Date(Math.max(this.startDate.getTime(), other.startDate.getTime())); const end = new Date(Math.min(this.endDate.getTime(), other.endDate.getTime())); return new Period(start, end, this.precision); } /** * Subtracts the given period from the current period and returns the resulting periods. * If the periods do not overlap, the current period is returned as a single-element array. * If they do overlap, the resulting periods are returned as an array. * * @param other - The period to subtract from the current period. * @returns An array of resulting periods after subtraction. */ subtract(other) { if (!this.overlapsWith(other)) { return [this]; } const periods = []; // Add period before the overlap (if any) if (this.startDate < other.startDate) { periods.push(new Period(this.startDate, other.startDate, this.precision)); } // Add period after the overlap (if any) if (this.endDate > other.endDate) { periods.push(new Period(other.endDate, this.endDate, this.precision)); } return periods; } /** * Calculates the gap between this period and another period. * If the periods overlap or are adjacent, returns null. * Otherwise, returns a new Period representing the gap between the two periods. * * @param other - The other period to compare with. * @returns A new Period representing the gap, or null if the periods overlap or are adjacent. */ gap(other) { if (this.overlapsWith(other) || this.isAdjacentTo(other)) { return null; } let start, end; if (this.endDate < other.startDate) { start = this.endDate; end = other.startDate; } else { start = other.endDate; end = this.startDate; } // Ensure start is before end for valid Period creation if (start >= end) { return null; } return new Period(start, end, this.precision); } /** * Computes the symmetric difference between this period and another period. * The symmetric difference is defined as the set of periods that are in either * of the two periods but not in their intersection. * * @param other - The other period to compute the symmetric difference with. * @returns An array of periods representing the symmetric difference, sorted * by start date and merged if adjacent. */ symmetricDifference(other) { const periods = []; const overlapPeriod = this.overlap(other); if (!overlapPeriod) { // No overlap, return both periods return [this, other].sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); } const thisSubtracted = this.subtract(overlapPeriod); const otherSubtracted = other.subtract(overlapPeriod); periods.push(...thisSubtracted, ...otherSubtracted); // Sort periods and merge adjacent ones return this.mergeAdjacentPeriods(periods); } mergeAdjacentPeriods(periods) { if (periods.length <= 1) return periods; periods.sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); const merged = [periods[0]]; for (let i = 1; i < periods.length; i++) { const current = periods[i]; const previous = merged[merged.length - 1]; if (previous.isAdjacentTo(current) || previous.overlapsWith(current)) { merged[merged.length - 1] = new Period(previous.startDate, current.endDate > previous.endDate ? current.endDate : previous.endDate, this.precision); } else { merged.push(current); } } return merged; } /** * Renews the current period by creating a new `Period` instance. * The new period starts immediately after the end of the current period * and has the same length and precision. * * @returns {Period} A new `Period` instance with updated start and end dates. */ renew() { const length = this.length(); const precisionMs = (0, precision_1.getPrecisionInMilliseconds)(this.precision); const newStart = new Date(this.endDate.getTime() + precisionMs); const newEnd = new Date(newStart.getTime() + length * precisionMs); return new Period(newStart, newEnd, this.precision, this.interval); } /** * Computes the union of this period with another period. * If the periods overlap or are adjacent, they are merged into a single period. * Otherwise, the periods are returned as separate, sorted periods. * * @param other - The other period to union with this period. * @returns An array of periods resulting from the union operation. */ union(other) { if (this.overlapsWith(other) || this.isAdjacentTo(other)) { const start = new Date(Math.min(this.startDate.getTime(), other.startDate.getTime())); const end = new Date(Math.max(this.endDate.getTime(), other.endDate.getTime())); return [new Period(start, end, this.precision)]; } return [this, other].sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); } // Fluent API methods /** * Sets the start date for the period. * * @param start - The start date, which can be a string or a Date object. * @returns The current instance for method chaining. * @throws {Error} If the new start date is after the current end date. */ setStart(start) { const newStartDate = this.validateDate(start); if (newStartDate > this.endDate) { throw new Error('Start date must be before or equal to end date'); } this.startDate = newStartDate; return this; } /** * Sets the end date for the period. * * @param end - The end date as a string or Date object. * @returns The current instance for method chaining. * @throws {Error} If the new end date is before the current start date. */ setEnd(end) { const newEndDate = this.validateDate(end); if (newEndDate < this.startDate) { throw new Error('End date must be after or equal to start date'); } this.endDate = newEndDate; return this; } /** * Sets the precision for the period. * * @param precision - The precision to set. * @returns The current instance for method chaining. */ setPrecision(precision) { this.precision = precision; return this; } /** * Sets the interval for the period. * * @param interval - The interval to be set. * @returns The current instance of the period. */ setInterval(interval) { this.interval = interval; return this; } } exports.Period = Period;