Multi-party Availability
Generating slots tells you when a schedule is open. Booking a meeting needs more than that: you have to know when everyone is actually free. That means removing the time that is already booked and then finding the overlap across several people’s calendars.
This is the part teams usually hand-roll, and it is easy to get wrong. timeslottr
gives you two interval functions for it, subtract and
intersect, plus generateAvailableTimeslots,
which wires them into the slot generator.
Everything here uses half-open [start, end) intervals (see
Core Concepts), so a slot that ends when a meeting starts
is not treated as a conflict.
The pipeline
base availability -> subtract(busy) -> intersect(participants) -> slots- Start from base availability: your
rangeminus buffers andexcludedWindows. - Subtract
busyto remove events that are already booked for everyone. - Intersect the participants to keep only windows where every person is free.
- Run the normal generator over what is left.
All in one call
generateAvailableTimeslots takes the same config as generateTimeslots, plus
two optional fields:
import { generateAvailableTimeslots } from 'timeslottr'
const slots = generateAvailableTimeslots({
day: '2024-01-01',
timezone: 'America/New_York',
range: { start: '09:00', end: '17:00' },
slotDurationMinutes: 30,
// booked events on the host's calendar, blocked for everyone
busy: [{ start: '12:00', end: '13:00' }],
// each invitee's busy times; only times where all are free survive
participantsBusy: [
[{ start: '09:00', end: '10:00' }], // Alice
[{ start: '16:00', end: '17:00' }], // Bob
],
})
// 30-minute slots between 10:00 and 16:00, with lunch removedbusy and participantsBusy accept the same formats as range and
excludedWindows: a Date, an ISO string, a time-only string like '09:00', or
a { date, time } object.
A fully booked day returns []. An empty inner array in participantsBusy
means that person is free for the whole window. Time-only busy strings need a
day or dated boundaries, the same as excludedWindows.
The primitives on their own
Sometimes you already have free and busy intervals and just want the math. Both
functions work on resolved Interval objects ({ start: Date; end: Date }).
subtract: availability minus bookings
import { subtract } from 'timeslottr'
const free = subtract(
[{ start: new Date('2024-01-01T09:00:00Z'), end: new Date('2024-01-01T17:00:00Z') }],
[{ start: new Date('2024-01-01T12:00:00Z'), end: new Date('2024-01-01T13:00:00Z') }],
)
// result: [09:00 to 12:00, 13:00 to 17:00]Busy intervals can be unsorted, overlap each other, or touch boundaries. A busy
window in the middle of a source splits it in two. It runs in O((n + m) log m),
not the nested loop people usually reach for first.
intersect: a time for the whole team
import { intersect } from 'timeslottr'
const team = intersect([
[{ start: new Date('2024-01-01T09:00:00Z'), end: new Date('2024-01-01T15:00:00Z') }],
[{ start: new Date('2024-01-01T11:00:00Z'), end: new Date('2024-01-01T17:00:00Z') }],
])
// result: [11:00 to 15:00]Given one set of free intervals per person, intersect returns the windows where
all of them overlap. With no sets it returns []. With one set it returns a
merged copy. If any set is empty, the result is [], since a person with no
availability blocks everyone. It runs in O(total intervals · log).
Try it
The Playground lets you add booked time and watch the available slots update as you go.