@chaisser/date-range
v1.0.1
Published
Date range utilities: overlap, contains, intersection
Maintainers
Readme
@chaisser/date-range
Date range utilities: overlap, contains, intersection
Create, compare, split, shift, and merge date ranges. Zero dependencies, full TypeScript support.
Installation
npm install @chaisser/date-range
# or
yarn add @chaisser/date-range
# or
pnpm add @chaisser/date-rangeQuick Start
import {
createDateRange,
overlaps,
contains,
intersection,
difference,
durationDays,
humanizeDuration
} from '@chaisser/date-range';
const sprint1 = createDateRange('2024-01-01', '2024-01-14');
const sprint2 = createDateRange('2024-01-10', '2024-01-24');
overlaps(sprint1, sprint2); // true
intersection(sprint1, sprint2); // { start: Jan 10, end: Jan 14 }
contains(sprint1, '2024-01-07'); // true
durationDays(sprint1); // 13
humanizeDuration(sprint1); // '1 week'API Reference
Create
| Function | Description |
|----------|-------------|
| createDateRange(start, end) | Create range (throws if invalid) |
| createDateRangeSafe(start, end) | Create range (returns null if invalid) |
| fromDateRange(range) | Shallow copy |
Accepts Date, string, or number (timestamp) as input.
Compare
| Function | Returns | Description |
|----------|---------|-------------|
| isEqual(a, b) | boolean | Same start and end |
| isBefore(a, b) | boolean | a ends before b starts |
| isAfter(a, b) | boolean | a starts after b ends |
| overlaps(a, b) | boolean | Ranges overlap (exclusive of boundaries) |
| touches(a, b) | boolean | Ranges are adjacent |
| overlapsOrTouches(a, b) | boolean | Overlap or adjacent |
| isSubset(a, b) | boolean | a is within b |
| isSuperset(a, b) | boolean | a contains b |
| isStrictSubset(a, b) | boolean | a is within b but not equal |
| isStrictSuperset(a, b) | boolean | a contains b but not equal |
| compare(a, b) | -1 \| 0 \| 1 | Sort order |
Contains
| Function | Description |
|----------|-------------|
| contains(range, date) | Date is within range (inclusive) |
| containsExclusive(range, date) | Date is strictly inside (exclusive) |
| containsStart(range, date) | Date is the start boundary |
| containsEnd(range, date) | Date is the end boundary |
| containsRange(outer, inner) | Outer fully contains inner |
Intersection & Union
| Function | Description |
|----------|-------------|
| intersection(a, b) | Overlapping portion (or null) |
| union(a, b) | Merged range if overlapping/touching (or null) |
| difference(a, b) | Parts of a not covered by b |
| symmetricDifference(a, b) | Parts in a or b but not both |
Duration
| Function | Returns |
|----------|---------|
| durationMs(range) | Milliseconds |
| durationSeconds(range) | Seconds |
| durationMinutes(range) | Minutes |
| durationHours(range) | Hours |
| durationDays(range) | Days |
| durationWeeks(range) | Weeks |
| durationMonths(range) | Months (approximate) |
| durationYears(range) | Years (approximate) |
| humanizeDuration(range) | Human-readable string (e.g., "3 weeks") |
Iterate
| Function | Description |
|----------|-------------|
| iterateDays(range) | Generator yielding each day |
| iterateWeeks(range) | Generator yielding weekly sub-ranges |
| iterateMonths(range) | Generator yielding monthly sub-ranges |
| toArrayDays(range) | Array of all days |
| toArrayWeeks(range) | Array of weekly sub-ranges |
| toArrayMonths(range) | Array of monthly sub-ranges |
Split
| Function | Description |
|----------|-------------|
| splitAt(range, date) | Split into two at a date |
| splitByDays(range, n) | Split into N-day chunks |
| splitByMonths(range, n) | Split into N-month chunks |
| splitInto(range, n) | Split into N equal parts |
Shift
| Function | Description |
|----------|-------------|
| addMs(range, ms) | Shift by milliseconds |
| addDays(range, n) | Shift forward by days |
| addWeeks(range, n) | Shift forward by weeks |
| addMonths(range, n) | Shift forward by months |
| addYears(range, n) | Shift forward by years |
| subtractDays(range, n) | Shift backward by days |
| subtractWeeks(range, n) | Shift backward by weeks |
| subtractMonths(range, n) | Shift backward by months |
| subtractYears(range, n) | Shift backward by years |
Expand & Shrink
| Function | Description |
|----------|-------------|
| expandStart(range, date) | Extend start if date is earlier |
| expandEnd(range, date) | Extend end if date is later |
| expandToInclude(range, date) | Extend to include date |
| shrinkStart(range, date) | Move start forward |
| shrinkEnd(range, date) | Move end backward |
Stats
| Function | Description |
|----------|-------------|
| countDays(range) | Total calendar days |
| countWeekdays(range) | Weekday count (Mon-Fri) |
| countWeekends(range) | Weekend count (Sat-Sun) |
| countSundays(range) | Sunday count |
| countSaturdays(range) | Saturday count |
| getStats(range) | { totalDays, weekdays, weekends, weeks, months, years } |
Validate
| Function | Description |
|----------|-------------|
| isValid(range) | start <= end |
| isEmpty(range) | Zero-duration range |
| isValidDate(date) | Date parses without NaN |
Multi-Range
| Function | Description |
|----------|-------------|
| mergeRanges(ranges) | Merge overlapping/touching ranges |
| intersectRanges(ranges) | Common intersection of all ranges |
| unionRanges(ranges) | Alias for mergeRanges |
| totalDurationMs(ranges) | Total ms after merging |
| totalDurationDays(ranges) | Total days after merging |
Format
| Function | Description |
|----------|-------------|
| formatRange(range, separator?) | "Jan 1, 2024 - Jan 31, 2024" |
| toISOString(range) | { start, end } as ISO strings |
| toString(range) | Alias for formatRange |
Factory Helpers
| Function | Returns |
|----------|---------|
| today() | Range spanning today |
| yesterday() | Range spanning yesterday |
| tomorrow() | Range spanning tomorrow |
| thisWeek() | Range for current week (Sun-Sat) |
| thisMonth() | Range for current month |
| thisYear() | Range for current year |
| lastNDays(n) | Last N days including today |
| nextNDays(n) | Next N days including today |
Examples
Find Scheduling Conflicts
import { createDateRange, overlaps, intersection, humanizeDuration } from '@chaisser/date-range';
const meeting1 = createDateRange('2024-01-15T09:00:00', '2024-01-15T10:00:00');
const meeting2 = createDateRange('2024-01-15T09:30:00', '2024-01-15T10:30:00');
if (overlaps(meeting1, meeting2)) {
const conflict = intersection(meeting1, meeting2)!;
console.log(`Conflict: ${humanizeDuration(conflict)}`);
// "Conflict: 30 minutes"
}Merge Availabilities
import { mergeRanges, totalDurationDays, difference } from '@chaisser/date-range';
const pto = [
createDateRange('2024-01-01', '2024-01-05'),
createDateRange('2024-01-03', '2024-01-08'),
createDateRange('2024-01-20', '2024-01-25')
];
const merged = mergeRanges(pto);
// [{ Jan 1 - Jan 8 }, { Jan 20 - Jan 25 }]
console.log(`Total PTO: ${totalDurationDays(merged)} days`);Split into Weekly Buckets
import { createDateRange, splitByDays, toArrayWeeks } from '@chaisser/date-range';
const quarter = createDateRange('2024-01-01', '2024-03-31');
// By 7-day chunks
const weeks = splitByDays(quarter, 7);
// Or use weekly iteration
for (const week of toArrayWeeks(quarter)) {
console.log(week.start.toISOString(), '-', week.end.toISOString());
}License
MIT
