Check if Two Date Ranges Overlap in JavaScript

To test whether two half-open date ranges overlap, check aStart < bEnd && bStart < aEnd — true only when they share at least one day. Part of Working with Date Ranges and Intervals.

Why this scenario is tricky

The overlap test people reach for first — checking whether either range's start falls inside the other — misses the case where one range fully contains the other, and it double-checks the case where they merely touch. The correct condition is famously terse but easy to get backwards: two half-open ranges [aStart, aEnd) and [bStart, bEnd) overlap exactly when aStart < bEnd and bStart < aEnd. The strict < (not <=) is what makes touching ranges — where one ends exactly as the other begins — count as not overlapping, which is almost always what a scheduler wants.

One condition covers every arrangementChecking 'is a start inside b?' misses containmentOne condition covers every arrangementaStart in b OR bStart in amisses full containmentaStart < bEnd && bStart < aEndhandles every caseThe two-comparison rule is total; the 'start inside' shortcut has blind spots.

Minimal working solution

Compare the endpoints with Temporal.PlainDate.compare. Touching ranges do not overlap under the half-open convention.

import { Temporal } from '@js-temporal/polyfill';
const lt = (x: Temporal.PlainDate, y: Temporal.PlainDate) => Temporal.PlainDate.compare(x, y) < 0;

function overlaps(aS, aE, bS, bE: Temporal.PlainDate) {
  // Overlap iff a starts before b ends AND b starts before a ends.
  return lt(aS, bE) && lt(bS, aE);
}

The overlap conditionTwo strict comparisons decide itThe overlap conditiona: [aS,aE)b: [bS,bE)aS < bE&& bS < aEoverlap?true/false

Full production version

A production helper validates each range, returns whether they overlap, and optionally the overlapping sub-range (useful for merging calendars).

import { Temporal } from '@js-temporal/polyfill';
type R = { start: Temporal.PlainDate; end: Temporal.PlainDate };
const cmp = Temporal.PlainDate.compare;

function intersection(a: R, b: R): R | null {
  if (cmp(a.start, a.end) > 0 || cmp(b.start, b.end) > 0) throw new RangeError('inverted range');
  const start = cmp(a.start, b.start) >= 0 ? a.start : b.start; // later start
  const end   = cmp(a.end, b.end) <= 0 ? a.end : b.end;         // earlier end
  return cmp(start, end) < 0 ? { start, end } : null;            // empty => no overlap
}

Compute the intersectionLater start, earlier end; empty means no overlapCompute the intersectionranges a,bmax(starts)min(ends)start<end?intersection

Verification

Overlap assertionsEvery arrangement asserts correctlyAssertions that prove the edge case[1,5) vs [3,7)overlap[1,5) vs [5,9)touching → false[1,9) vs [3,5)contained → true[1,3) vs [5,7)disjoint → false

Common pitfalls

Overlap pitfallsWrong operator or one-sided testWrongRightuse <= on the boundarytouching counts as overlapstrict < with half-opentouching = no overlaptest only 'aStart in b'misses containmentaS<bE && bS<aEtotal condition

Frequently Asked Questions

What is the simplest way to check if two date ranges overlap?

Two half-open ranges overlap when the first starts before the second ends and the second starts before the first ends: aStart < bEnd && bStart < aEnd. Use strict less-than so ranges that merely touch at a boundary are treated as non-overlapping.

Do ranges that touch at an endpoint overlap?

Under the half-open [start, end) convention they do not. Because the end is exclusive, a range ending on the same day another begins shares no day, so the strict comparison returns false. That is what you want for back-to-back bookings.