Skip to Content
GuideMulti-party Availability

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
  1. Start from base availability: your range minus buffers and excludedWindows.
  2. Subtract busy to remove events that are already booked for everyone.
  3. Intersect the participants to keep only windows where every person is free.
  4. 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 removed

busy 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.

Last updated on