Core concepts
Half-open intervals [start, end)
Every range, slot, exclusion, and availability window is half-open. The start
is inclusive and the end is exclusive.
import { contains, createTimeslot } from 'timeslottr'
const slot = createTimeslot(
new Date('2024-01-01T09:00:00Z'),
new Date('2024-01-01T10:00:00Z'),
)
contains(slot, new Date('2024-01-01T09:00:00Z')) // true (start is inclusive)
contains(slot, new Date('2024-01-01T10:00:00Z')) // false (end is exclusive)This is what stops adjacent slots from overlapping. A 9:00 to 10:00 slot and a
10:00 to 11:00 slot both touch 10:00, but the first one excludes its end, so they
do not conflict. overlaps, subtract, and intersect all follow the same
rule, so booked time that ends exactly when a slot begins does not block it.
The Timeslot shape
generateTimeslots and the other generators return Timeslot[]:
interface Timeslot {
start: Date // inclusive
end: Date // exclusive
metadata?: {
index: number // 0-based position in the result
durationMinutes: number
label?: string // from labelFormatter, if provided
}
}start and end are real Date instants, stored as UTC. To send them over the
wire, use timeslotToJSON and timeslotFromJSON. Plain
JSON.stringify turns the dates into strings and will not parse them back into
Date objects.
Timezones and DST
You can write a boundary as a time-only string ('09:00'), a full ISO string, a
Date, or a { date, time } object. When you pass a timezone such as
'America/New_York', time-only boundaries are resolved in that zone and
converted to the correct UTC instant. This runs on the platform Intl API, so
there is no date library involved.
Date-only values must be written YYYY-MM-DD, which is resolved in timezone.
Looser forms like '2025-3-5' or '2025/03/05' are rejected: the engine would
resolve them against the machine’s local zone rather than timezone, so the same
input would mean different days on different machines. Strings that carry an
explicit time are unaffected. A day past the end of its month ('2025-02-30') is
also rejected instead of rolling over into the next month.
Daylight saving is handled for you. On a spring-forward day the 2:00 to 3:00 wall-clock hour does not exist, so a 00:00 to 06:00 range produces five hours of slots, not six:
const slots = generateTimeslots({
day: '2024-03-10', // US spring-forward
timezone: 'America/New_York',
range: { start: '00:00', end: '06:00' },
slotDurationMinutes: 30,
})
slots.length // 10: six wall-clock hours, but only five real onesDurations are measured in real elapsed time. Every 30-minute slot above is exactly 30 minutes long, including the one that spans the DST gap.
Time-only boundaries need a calendar date to anchor them. Pass day for a single
day, or let generateDailyTimeslots set it per day. See
Scheduling.