From ea382fb8eb30ac381db39db17211dc89f023ec0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 15:43:29 +0200 Subject: [PATCH 01/12] update docs to support devs better and give better overview about new scheduling --- docs/api-reference.md | 39 +++++++++++++---- docs/availability-api.md | 26 +++++------ docs/core-concepts.md | 94 +++++++++++++++++++++++++++++++++++----- docs/examples.md | 3 +- docs/getting-started.md | 39 ++++++++++++++--- 5 files changed, 161 insertions(+), 40 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 6d1e988..d0e02f6 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -42,10 +42,19 @@ Configuration options for slot generation. ```typescript interface SchedulingOptions { + // Required slotDuration: number + + // Optional slotSplit?: number padding?: number offset?: number + maxOverlaps?: number + + // Daily window filtering (timezone-aware) + timezone?: string + earliestTime?: string | number // 'HH:mm' or minutes since midnight + latestTime?: string | number // 'HH:mm' or minutes since midnight; supports '24:00' or 1440 } ``` @@ -55,6 +64,9 @@ interface SchedulingOptions { - `slotSplit` (optional): Interval between the start times of consecutive slots in minutes. Defaults to `slotDuration`. Must be positive. - `padding` (optional): Buffer time in minutes to add before and after each busy time. Defaults to 0. Must be non-negative. - `offset` (optional): Offset from standard time boundaries in minutes. Defaults to 0. Must be non-negative. +- `maxOverlaps` (optional): Allow up to K overlapping busy intervals. If undefined, traditional behavior applies (any overlap blocks a slot). +- `timezone` (optional): IANA timezone identifier used for daily window filtering. Required if `earliestTime`/`latestTime` are provided when using the core `Scheduler`. +- `earliestTime`/`latestTime` (optional): Local daily time window for slot START times. Specify as `HH:mm` or minutes since midnight. `latestTime` accepts `"24:00"` or `1440` as end of day. ### Availability Types @@ -91,14 +103,12 @@ Defines a weekly availability pattern with multiple schedules. ```typescript interface WeeklyAvailability { schedules: DaySchedule[] - timezone?: string } ``` **Properties:** - `schedules`: Array of availability schedules. Must contain at least one schedule. -- `timezone` (optional): IANA timezone identifier (e.g., 'America/New_York', 'Europe/London') ## Functions @@ -256,12 +266,13 @@ Enhanced scheduler that combines weekly availability patterns with traditional b ### Constructor ```typescript -constructor(availability?: WeeklyAvailability, existingBusyTimes?: BusyTime[]) +constructor(availability?: WeeklyAvailability, timezone?: string, existingBusyTimes?: BusyTime[]) ``` **Parameters:** - `availability` (optional): Weekly availability pattern defining when slots are available +- `timezone` (optional): IANA timezone identifier for availability processing and daily window fallback - `existingBusyTimes` (optional): Array of existing busy times to include **Throws:** @@ -278,9 +289,14 @@ const scheduler = new AvailabilityScheduler({ schedules: [{ days: ['monday'], start: '09:00', end: '17:00' }], }) -// Create with availability and existing busy times +// Create with availability and timezone +const schedulerTz = new AvailabilityScheduler({ + schedules: [{ days: ['monday'], start: '09:00', end: '17:00' }], +}, 'America/New_York') + +// Create with availability, timezone, and existing busy times const busyTimes = [{ start: new Date('2024-01-01T10:00:00Z'), end: new Date('2024-01-01T11:00:00Z') }] -const scheduler = new AvailabilityScheduler(availability, busyTimes) +const schedulerWithAll = new AvailabilityScheduler(availability, 'Europe/London', busyTimes) ``` ### setAvailability @@ -304,7 +320,6 @@ setAvailability(availability: WeeklyAvailability): void ```typescript scheduler.setAvailability({ schedules: [{ days: ['monday', 'tuesday'], start: '09:00', end: '17:00' }], - timezone: 'America/New_York', }) ``` @@ -417,13 +432,18 @@ const slots = scheduler.findAvailableSlots(new Date('2024-01-15T08:00:00Z'), new Converts a weekly availability pattern into busy times for a specific week. ```typescript -function weeklyAvailabilityToBusyTimes(availability: WeeklyAvailability, weekStart: Date): BusyTime[] +function weeklyAvailabilityToBusyTimes( + availability: WeeklyAvailability, + weekStart: Date, + timezone?: string +): BusyTime[] ``` **Parameters:** - `availability`: The weekly availability pattern to convert - `weekStart`: The Monday date for the week to process (MUST be a Monday) +- `timezone` (optional): IANA timezone used to interpret availability times. Falls back to `process.env.SCHEDULING_TIMEZONE` or `UTC`. **Returns:** @@ -483,6 +503,9 @@ All methods perform input validation and will throw descriptive errors for inval - `slotSplit` must be a positive number (if provided) - `padding` must be a non-negative number (if provided) - `offset` must be a non-negative number (if provided) +- `maxOverlaps` must be a non-negative integer (if provided) +- If `earliestTime` or `latestTime` is provided, `timezone` must also be specified when using the core `Scheduler` +- `earliestTime`/`latestTime` must be valid times (string `HH:mm` or minutes). `latestTime` may be `24:00`/`1440`. - All numeric values must be finite ### Availability Validation @@ -493,7 +516,7 @@ All methods perform input validation and will throw descriptive errors for inval - Time format must be HH:mm (24-hour format) - Start time must be before end time - No overlapping schedules on the same day -- Timezone must be valid IANA format (if provided) + ## Behavior Details diff --git a/docs/availability-api.md b/docs/availability-api.md index 0fa5264..1620f12 100644 --- a/docs/availability-api.md +++ b/docs/availability-api.md @@ -69,14 +69,12 @@ interface DaySchedule { ```typescript interface WeeklyAvailability { schedules: DaySchedule[] // Array of availability schedules - timezone?: string // Optional IANA timezone identifier } ``` **Key Requirements:** - `schedules`: Must contain at least one schedule -- `timezone`: Optional IANA timezone (e.g., "America/New_York", "Europe/London") ## AvailabilityScheduler Class @@ -85,7 +83,7 @@ The main class for working with weekly availability patterns. ### Constructor ```typescript -constructor(availability?: WeeklyAvailability, existingBusyTimes: BusyTime[] = []) +constructor(availability?: WeeklyAvailability, timezone?: string, existingBusyTimes: BusyTime[] = []) ``` Creates a new availability scheduler with optional initial availability and existing busy times. @@ -93,6 +91,7 @@ Creates a new availability scheduler with optional initial availability and exis **Parameters:** - `availability` (optional): The weekly availability pattern +- `timezone` (optional): IANA timezone identifier used for availability processing and daily window fallback - `existingBusyTimes` (optional): Array of existing busy times to include **Example:** @@ -102,7 +101,7 @@ const availability = { schedules: [{ days: ['monday', 'tuesday'], start: '09:00', end: '17:00' }], } -const scheduler = new AvailabilityScheduler(availability) +const scheduler = new AvailabilityScheduler(availability, 'America/New_York') ``` ### Methods @@ -201,7 +200,7 @@ const slots = scheduler.findAvailableSlots(new Date('2024-01-15T08:00:00Z'), new ## Helper Functions -### `weeklyAvailabilityToBusyTimes(availability: WeeklyAvailability, weekStart: Date): BusyTime[]` +### `weeklyAvailabilityToBusyTimes(availability: WeeklyAvailability, weekStart: Date, timezone?: string): BusyTime[]` Converts a weekly availability pattern into busy times for a specific week. @@ -209,6 +208,7 @@ Converts a weekly availability pattern into busy times for a specific week. - `availability`: The weekly availability pattern - `weekStart`: **Must be a Monday** (Date.getDay() === 1) +- `timezone` (optional): IANA timezone identifier used to interpret availability times. Falls back to `process.env.SCHEDULING_TIMEZONE` or `UTC`. **Returns:** Array of busy times representing unavailable periods @@ -272,10 +272,9 @@ const businessHours = { end: '17:00', }, ], - timezone: 'America/New_York', } -const scheduler = new AvailabilityScheduler(businessHours) +const scheduler = new AvailabilityScheduler(businessHours, 'America/New_York') // Find 1-hour appointment slots for this week const slots = scheduler.findAvailableSlots(new Date('2024-01-15T00:00:00Z'), new Date('2024-01-19T23:59:59Z'), { @@ -364,14 +363,13 @@ scheduler.setAvailability({ ### 2. Handling Timezones ```typescript -// ✅ Good - specify timezone when working across timezones -const availability = { - schedules: [...], - timezone: 'America/Los_Angeles' -} +// ✅ Specify timezone at the scheduler level +const availability = { schedules: [...] } +const scheduler = new AvailabilityScheduler(availability, 'America/Los_Angeles') -// ✅ Good - convert dates to appropriate timezone before processing -const localMondayStart = new Date('2024-01-01T08:00:00Z') // UTC Monday 8 AM +// ✅ Or specify timezone when converting availability for a week +const localMondayStart = new Date('2024-01-01T00:00:00Z') // Monday UTC +const busyTimes = weeklyAvailabilityToBusyTimes(availability, localMondayStart, 'America/Los_Angeles') ``` ### 3. Break Management diff --git a/docs/core-concepts.md b/docs/core-concepts.md index 139c095..7b6b5be 100644 --- a/docs/core-concepts.md +++ b/docs/core-concepts.md @@ -84,10 +84,19 @@ The `SchedulingOptions` interface provides fine-grained control over how slots a ```typescript interface SchedulingOptions { - slotDuration: number // Required - slotSplit?: number // Optional - padding?: number // Optional - offset?: number // Optional + // Required + slotDuration: number + + // Optional + slotSplit?: number + padding?: number + offset?: number + maxOverlaps?: number + + // Daily window filtering (timezone-aware) + timezone?: string + earliestTime?: string | number // 'HH:mm' or minutes since midnight + latestTime?: string | number // 'HH:mm' or minutes since midnight; supports '24:00' or 1440 } ``` @@ -181,6 +190,39 @@ The `offset` parameter shifts slot start times from standard boundaries (in minu **Default:** 0 (align to standard boundaries) +### Daily Windows + +The `earliestTime` and `latestTime` parameters control the daily window for slot generation. + +```typescript +// Restrict slots to 9:00 AM - 5:00 PM +{ + earliestTime: '09:00', + latestTime: '17:00' +} + +// Restrict slots to 10:00 AM - 6:00 PM with 24-hour clock support +{ + earliestTime: '10:00', + latestTime: '24:00' +} +``` + +**Default:** No daily window restrictions + +### K-overlaps + +The `maxOverlaps` parameter allows up to K overlapping busy intervals. + +```typescript +// Allow one overlapping busy time +{ + maxOverlaps: 1 +} +``` + +**Default:** 0 (no overlapping busy times) + ## Weekly Availability The **weekly availability** system allows you to define recurring weekly patterns that specify when time slots are available for scheduling. This is perfect for businesses, professionals, or services that operate on predictable weekly schedules. @@ -255,16 +297,48 @@ The availability system processes one week at a time, starting from Monday: ### Timezone Support -You can specify timezone information for your availability patterns: +Timezone is specified at the scheduler level (not inside `WeeklyAvailability`). ```typescript -{ - schedules: [...], - timezone: 'America/New_York' // IANA timezone identifier -} +const availability = { schedules: [...] } +const scheduler = new AvailabilityScheduler(availability, 'America/New_York') +``` + +- The `AvailabilityScheduler` uses its constructor timezone for availability conversion and as a fallback for daily-window filtering if you omit `options.timezone`. +- The core `Scheduler` requires `options.timezone` when you use `earliestTime`/`latestTime`. + +Daily window filtering supports `'HH:mm'` strings or minute numbers, with `latestTime` accepting `'24:00'` or `1440`. + +```typescript +// Core Scheduler daily windows +createScheduler().findAvailableSlots(start, end, { + slotDuration: 30, + timezone: 'America/New_York', + earliestTime: '09:00', + latestTime: '17:00', +}) + +// AvailabilityScheduler daily windows (timezone fallback from constructor) +new AvailabilityScheduler(availability, 'UTC').findAvailableSlots(start, end, { + slotDuration: 30, + earliestTime: 9 * 60, + latestTime: '24:00', +}) +``` + +### Allowing Overlaps (K-overlaps) + +You can allow up to `K` overlapping busy intervals by providing `maxOverlaps`. + +```typescript +createScheduler().findAvailableSlots(start, end, { + slotDuration: 30, + slotSplit: 15, + maxOverlaps: 1, // allow one overlapping busy time +}) ``` -**Note**: Timezone is primarily for documentation and future timezone-aware features. The current implementation processes dates as provided. +When `maxOverlaps` is set, an optimized algorithm finds free periods before slot generation. Daily-window filtering and timezone rules still apply during slot generation. ## Algorithm Overview diff --git a/docs/examples.md b/docs/examples.md index b5a27c5..1a84f92 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -320,10 +320,9 @@ const medicalSchedule = { { days: ['saturday'], start: '09:00', end: '13:00' }, // Sunday is automatically unavailable (no schedule) ], - timezone: 'America/New_York', } -const doctorScheduler = new AvailabilityScheduler(medicalSchedule) +const doctorScheduler = new AvailabilityScheduler(medicalSchedule, 'America/New_York') // Find 30-minute appointment slots with 10-minute buffer const appointments = doctorScheduler.findAvailableSlots( diff --git a/docs/getting-started.md b/docs/getting-started.md index 022d691..d28c66c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -97,13 +97,28 @@ Configure how slots are generated: ```typescript interface SchedulingOptions { + // Required slotDuration: number // Duration of each slot in minutes + + // Optional slotSplit?: number // Interval between slot starts (default: same as duration) - padding?: number // Buffer time around busy periods in minutes - offset?: number // Offset from hour boundaries in minutes + padding?: number // Buffer time around busy periods in minutes + offset?: number // Offset from hour boundaries in minutes + maxOverlaps?: number // Allow up to K overlapping busy intervals (K-overlaps) + + // Daily window filtering (timezone-aware) + timezone?: string + earliestTime?: string | number // 'HH:mm' or minutes since midnight + latestTime?: string | number // 'HH:mm' or minutes since midnight; supports '24:00' or 1440 } ``` +Quick links: + +- [API Reference › SchedulingOptions](api-reference.md#schedulingoptions) +- [Core Concepts › Daily Windows](core-concepts.md#daily-windows) +- [Core Concepts › K-overlaps](core-concepts.md#k-overlaps) + ## Common Use Cases ### 1. Simple Appointment Booking @@ -145,6 +160,17 @@ const slots = scheduler.findAvailableSlots(new Date('2024-01-15T09:00:00Z'), new }) ``` +### 3b. Allowing Overlaps (K-overlaps) + +```typescript +// Allow up to 1 overlapping busy time (K = 1) +const kOverlapSlots = scheduler.findAvailableSlots(new Date('2024-01-15T09:00:00Z'), new Date('2024-01-15T17:00:00Z'), { + slotDuration: 30, + slotSplit: 15, + maxOverlaps: 1, +}) +``` + ### 4. Aligned Scheduling ```typescript @@ -191,7 +217,8 @@ try { ## Next Steps -- [API Reference](api-reference.md) - Complete method documentation -- [Core Concepts](core-concepts.md) - Detailed explanation of scheduling concepts -- [Examples](examples.md) - Real-world usage scenarios -- [Performance Guide](performance.md) - Optimization tips +- [API Reference](api-reference.md) — Complete method documentation +- [Core Concepts](core-concepts.md) — Daily Windows and K-overlaps +- [Availability API](availability-api.md) — Weekly schedules and scheduler-level timezone +- [Examples](examples.md) — Real-world usage scenarios +- [Performance Guide](performance.md) — Optimization tips From 0a84867ebc81617a158bf85ed2a0e8b12bb5e880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 15:52:55 +0200 Subject: [PATCH 02/12] remove the convenience functions --- README.md | 12 ++++++------ docs/api-reference.md | 26 ++++++-------------------- docs/core-concepts.md | 4 ++-- docs/getting-started.md | 14 +++++++------- src/index.ts | 14 -------------- 5 files changed, 21 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index c209684..4d22ba9 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ bun add scheduling-sdk ### Standard Scheduling ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' // Initialize scheduler with busy times (existing meetings, appointments, etc.) // Busy times are periods when you're NOT available -const scheduler = createScheduler([ +const scheduler = new Scheduler([ { start: new Date('2024-01-15T09:00:00Z'), // Meeting starts at 9:00 AM end: new Date('2024-01-15T10:00:00Z'), // Meeting ends at 10:00 AM @@ -83,9 +83,9 @@ const availableSlots = scheduler.findAvailableSlots( ### Managing Busy Times ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' -const scheduler = createScheduler() +const scheduler = new Scheduler() // Add a single busy time (e.g., a new meeting) scheduler.addBusyTime({ @@ -177,9 +177,9 @@ Restrict generated slots to specific local hours by providing a timezone and a d Core `Scheduler` usage: ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' -const scheduler = createScheduler() +const scheduler = new Scheduler() // Search the whole day in UTC, but only return slots that START between 9:00 and 17:00 New York time const slots = scheduler.findAvailableSlots( diff --git a/docs/api-reference.md b/docs/api-reference.md index d0e02f6..7015611 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -112,33 +112,19 @@ interface WeeklyAvailability { ## Functions -### createScheduler +### Creating a Scheduler -Creates a new Scheduler instance. +Instantiate the class directly. ```typescript -function createScheduler(busyTimes?: BusyTime[]): Scheduler -``` - -**Parameters:** - -- `busyTimes` (optional): Initial array of busy times. Defaults to empty array. - -**Returns:** - -- New `Scheduler` instance - -**Example:** - -```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' // Create empty scheduler -const scheduler = createScheduler() +const scheduler = new Scheduler() // Create scheduler with initial busy times -const busyScheduler = createScheduler([ - { start: new Date('2024-01-15T10:00:00Z'), end: new Date('2024-01-15T11:00:00Z') }, +const busyScheduler = new Scheduler([ + { start: new Date('2024-01-15T10:00:00Z'), end: new Date('2024-01-15T11:00:00Z') }, ]) ``` diff --git a/docs/core-concepts.md b/docs/core-concepts.md index 7b6b5be..1daf3a0 100644 --- a/docs/core-concepts.md +++ b/docs/core-concepts.md @@ -311,7 +311,7 @@ Daily window filtering supports `'HH:mm'` strings or minute numbers, with `lates ```typescript // Core Scheduler daily windows -createScheduler().findAvailableSlots(start, end, { +new Scheduler().findAvailableSlots(start, end, { slotDuration: 30, timezone: 'America/New_York', earliestTime: '09:00', @@ -331,7 +331,7 @@ new AvailabilityScheduler(availability, 'UTC').findAvailableSlots(start, end, { You can allow up to `K` overlapping busy intervals by providing `maxOverlaps`. ```typescript -createScheduler().findAvailableSlots(start, end, { +new Scheduler().findAvailableSlots(start, end, { slotDuration: 30, slotSplit: 15, maxOverlaps: 1, // allow one overlapping busy time diff --git a/docs/getting-started.md b/docs/getting-started.md index d28c66c..9476b36 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -26,10 +26,10 @@ The Scheduling SDK provides a simple interface for finding available time slots ### Basic Example ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' // Create a scheduler with busy times (periods when NOT available) -const scheduler = createScheduler([ +const scheduler = new Scheduler([ { start: new Date('2024-01-15T09:00:00Z'), // 9:00 AM meeting end: new Date('2024-01-15T10:00:00Z'), // ends at 10:00 AM @@ -61,7 +61,7 @@ console.log(availableSlots) ### Step-by-Step Breakdown -1. **Import the SDK**: Import the `createScheduler` function +1. **Import the SDK**: Import the `Scheduler` class 2. **Create Scheduler**: Initialize with existing busy times (optional) 3. **Define Options**: Configure slot duration, padding, splitting, and offset 4. **Find Slots**: Call `findAvailableSlots` with time range and options @@ -124,9 +124,9 @@ Quick links: ### 1. Simple Appointment Booking ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' -const scheduler = createScheduler() +const scheduler = new Scheduler() // Add existing appointments scheduler.addBusyTimes([ @@ -186,8 +186,8 @@ const slots = scheduler.findAvailableSlots(new Date('2024-01-15T09:00:00Z'), new ```typescript // Separate schedulers for different resources -const roomScheduler = createScheduler() -const equipmentScheduler = createScheduler() +const roomScheduler = new Scheduler() +const equipmentScheduler = new Scheduler() // Add resource-specific busy times roomScheduler.addBusyTimes([...roomBookings]) diff --git a/src/index.ts b/src/index.ts index 9043c09..e4a6ab0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,17 +37,3 @@ export { } from './validators/options.validator' // Export validators export { validateTimeRange } from './validators/time-range.validator' - -import { AvailabilityScheduler } from './availability/scheduler' -// Export convenience function -import { Scheduler } from './core/scheduler' -import type { WeeklyAvailability } from './types/availability.types' -import type { BusyTime } from './types/scheduling.types' - -export function createScheduler(busyTimes: BusyTime[] = []): Scheduler { - return new Scheduler(busyTimes) -} - -export function createAvailabilityScheduler(availability: WeeklyAvailability, timezone: string): AvailabilityScheduler { - return new AvailabilityScheduler(availability, timezone) -} From d8b8f61365155c4ce1d34c45efaf9dc1ee4f717a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 15:53:05 +0200 Subject: [PATCH 03/12] incr version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a172696..dbf94dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scheduling-sdk", - "version": "0.3.2", + "version": "0.4.0", "description": "Brought to you by Recal - A TypeScript SDK for scheduling functionality", "author": "Recal", "license": "MIT", From f4753175eefc0aed05a4ad6d3cbb139b777e88fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 15:54:50 +0200 Subject: [PATCH 04/12] fix constructors in docs/examples --- docs/examples.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 1a84f92..971b636 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -17,10 +17,10 @@ This document provides practical examples for common scheduling scenarios using ### Simple Appointment Booking ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' // Create scheduler with busy times (when you're NOT available) -const appointmentScheduler = createScheduler([ +const appointmentScheduler = new Scheduler([ { start: new Date('2024-01-15T10:00:00Z'), end: new Date('2024-01-15T11:00:00Z') }, // Morning meeting { start: new Date('2024-01-15T14:00:00Z'), end: new Date('2024-01-15T15:00:00Z') }, // Afternoon call ]) @@ -40,7 +40,7 @@ console.log(`Found ${availableSlots.length} available appointment slots`) ```typescript // Medical appointments need cleanup/prep time between patients -const doctorScheduler = createScheduler([ +const doctorScheduler = new Scheduler([ { start: new Date('2024-01-15T10:00:00Z'), end: new Date('2024-01-15T10:30:00Z') }, { start: new Date('2024-01-15T14:00:00Z'), end: new Date('2024-01-15T15:00:00Z') }, ]) @@ -57,7 +57,7 @@ const slots = doctorScheduler.findAvailableSlots(new Date('2024-01-15T09:00:00Z' ```typescript // Generate overlapping consultation slots for flexibility -const consultationScheduler = createScheduler() +const consultationScheduler = new Scheduler() const flexibleSlots = consultationScheduler.findAvailableSlots( new Date('2024-01-15T09:00:00Z'), @@ -76,10 +76,10 @@ const flexibleSlots = consultationScheduler.findAvailableSlots( ### Business Hours Scheduling ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' function createBusinessHoursScheduler() { - const scheduler = createScheduler() + const scheduler = new Scheduler() // Add non-business hours as busy times const today = new Date('2024-01-15T00:00:00Z') @@ -114,7 +114,7 @@ const meetingSlots = businessScheduler.findAvailableSlots( ```typescript function findSlotsAcrossWeek(existingMeetings, slotDuration = 60) { - const scheduler = createScheduler(existingMeetings) + const scheduler = new Scheduler(existingMeetings) const allSlots = [] // Schedule across work week @@ -154,7 +154,7 @@ class MeetingRoomScheduler { constructor(roomNames: string[]) { roomNames.forEach(name => { - this.rooms.set(name, createScheduler()) + this.rooms.set(name, new Scheduler()) }) } @@ -227,7 +227,7 @@ class EquipmentScheduler { constructor(equipment: { id: string; name: string; setupTime: number }[]) { equipment.forEach(item => { this.equipmentSchedulers.set(item.id, { - scheduler: createScheduler(), + scheduler: new Scheduler(), setupTime: item.setupTime, name: item.name, }) @@ -534,7 +534,7 @@ class TeamMeetingScheduler { private teamMembers = new Map() addTeamMember(name: string, busyTimes: BusyTime[] = []) { - this.teamMembers.set(name, createScheduler(busyTimes)) + this.teamMembers.set(name, new Scheduler(busyTimes)) } addMemberBusyTime(name: string, busyTime: BusyTime) { @@ -609,8 +609,8 @@ console.log(`Found ${teamSlots.length} slots where all team members are availabl ```typescript class PriorityScheduler { - private highPriorityScheduler = createScheduler() - private normalScheduler = createScheduler() + private highPriorityScheduler = new Scheduler() + private normalScheduler = new Scheduler() addHighPriorityBusyTime(busyTime: BusyTime) { this.highPriorityScheduler.addBusyTimes([busyTime]) @@ -646,7 +646,7 @@ class PriorityScheduler { ```typescript class RecurringScheduler { - private scheduler = createScheduler() + private scheduler = new Scheduler() addRecurringBusyTime(startTime: Date, endTime: Date, pattern: 'daily' | 'weekly' | 'monthly', occurrences: number) { const busyTimes = [] @@ -708,7 +708,7 @@ const slots = recurringScheduler.findAvailableSlots( ```typescript // Example integration with a calendar API class CalendarIntegration { - private scheduler = createScheduler() + private scheduler = new Scheduler() async syncWithCalendar(calendarApi: any, startDate: Date, endDate: Date) { // Fetch events from calendar API @@ -743,7 +743,7 @@ class CalendarIntegration { ```typescript // Example with database persistence class PersistentScheduler { - private scheduler = createScheduler() + private scheduler = new Scheduler() private db: any // Your database connection async loadBusyTimes(resourceId: string) { From 0901d0582c3972d68fd2d2fb92bc9666707217a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 15:56:49 +0200 Subject: [PATCH 05/12] streamline tests --- tests/validators/options.validator.test.ts | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/validators/options.validator.test.ts b/tests/validators/options.validator.test.ts index 73a3721..fbdc467 100644 --- a/tests/validators/options.validator.test.ts +++ b/tests/validators/options.validator.test.ts @@ -140,9 +140,12 @@ describe('Options Validator', () => { }) it('should reject non-numbers', () => { - expect(() => validateSplit('30' as unknown)).toThrow('Slot split must be a positive number') - expect(() => validateSplit(null as unknown)).toThrow('Slot split must be a positive number') - expect(() => validateSplit(undefined as unknown)).toThrow('Slot split must be a positive number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validateSplit('30' as any)).toThrow('Slot split must be a positive number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validateSplit(null as any)).toThrow('Slot split must be a positive number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validateSplit(undefined as any)).toThrow('Slot split must be a positive number') }) }) @@ -170,9 +173,12 @@ describe('Options Validator', () => { }) it('should reject non-numbers', () => { - expect(() => validateOffset('15' as unknown)).toThrow('Offset must be a non-negative number') - expect(() => validateOffset(null as unknown)).toThrow('Offset must be a non-negative number') - expect(() => validateOffset(undefined as unknown)).toThrow('Offset must be a non-negative number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validateOffset('15' as any)).toThrow('Offset must be a non-negative number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validateOffset(null as any)).toThrow('Offset must be a non-negative number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validateOffset(undefined as any)).toThrow('Offset must be a non-negative number') }) }) @@ -200,9 +206,12 @@ describe('Options Validator', () => { }) it('should reject non-numbers', () => { - expect(() => validatePadding('15' as unknown)).toThrow('Padding must be a non-negative number') - expect(() => validatePadding(null as unknown)).toThrow('Padding must be a non-negative number') - expect(() => validatePadding(undefined as unknown)).toThrow('Padding must be a non-negative number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validatePadding('15' as any)).toThrow('Padding must be a non-negative number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validatePadding(null as any)).toThrow('Padding must be a non-negative number') + // biome-ignore lint/suspicious/noExplicitAny: needed for negative tests + expect(() => validatePadding(undefined as any)).toThrow('Padding must be a non-negative number') }) }) From 631dc5d1a6c168c8437e8d962d96418d1439d9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 15:58:05 +0200 Subject: [PATCH 06/12] update tests to use constructors instead of convenience functions --- tests/index.test.ts | 59 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/tests/index.test.ts b/tests/index.test.ts index 737635e..91739bd 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -2,8 +2,6 @@ import { describe, expect, it } from 'bun:test' import { AvailabilityScheduler, type BusyTime, - createAvailabilityScheduler, - createScheduler, Scheduler, type SchedulingOptions, type TimeSlot, @@ -57,15 +55,15 @@ describe('Index Exports', () => { }) }) - describe('createScheduler convenience function', () => { + describe('Scheduler instantiation (as per README)', () => { it('should create a Scheduler instance with no arguments', () => { - const scheduler = createScheduler() + const scheduler = new Scheduler() expect(scheduler).toBeInstanceOf(Scheduler) expect(scheduler.getBusyTimes()).toEqual([]) }) it('should create a Scheduler instance with empty busy times', () => { - const scheduler = createScheduler([]) + const scheduler = new Scheduler([]) expect(scheduler).toBeInstanceOf(Scheduler) expect(scheduler.getBusyTimes()).toEqual([]) }) @@ -76,7 +74,7 @@ describe('Index Exports', () => { { start: new Date('2024-01-01T14:00:00Z'), end: new Date('2024-01-01T15:00:00Z') }, ] - const scheduler = createScheduler(busyTimes) + const scheduler = new Scheduler(busyTimes) expect(scheduler).toBeInstanceOf(Scheduler) expect(scheduler.getBusyTimes()).toEqual(busyTimes) }) @@ -86,7 +84,7 @@ describe('Index Exports', () => { { start: new Date('2024-01-01T12:00:00Z'), end: new Date('2024-01-01T13:00:00Z') }, ] - const scheduler = createScheduler(busyTimes) + const scheduler = new Scheduler(busyTimes) const startTime = new Date('2024-01-01T09:00:00Z') const endTime = new Date('2024-01-01T17:00:00Z') @@ -111,8 +109,8 @@ describe('Index Exports', () => { }) it('should return different instances on multiple calls', () => { - const scheduler1 = createScheduler() - const scheduler2 = createScheduler() + const scheduler1 = new Scheduler() + const scheduler2 = new Scheduler() expect(scheduler1).not.toBe(scheduler2) expect(scheduler1).toBeInstanceOf(Scheduler) @@ -120,8 +118,8 @@ describe('Index Exports', () => { }) it('should not share state between instances', () => { - const scheduler1 = createScheduler() - const scheduler2 = createScheduler() + const scheduler1 = new Scheduler() + const scheduler2 = new Scheduler() const busyTime: BusyTime = { start: new Date('2024-01-01T10:00:00Z'), @@ -137,13 +135,13 @@ describe('Index Exports', () => { describe('integration with exported functionality', () => { it('should work with all exported components together', () => { - // Create scheduler using convenience function + // Create scheduler using direct instantiation (as per README) const busyTimes: BusyTime[] = [ { start: new Date('2024-01-01T10:00:00Z'), end: new Date('2024-01-01T11:00:00Z') }, { start: new Date('2024-01-01T14:00:00Z'), end: new Date('2024-01-01T15:00:00Z') }, ] - const scheduler = createScheduler(busyTimes) + const scheduler = new Scheduler(busyTimes) // Define scheduling parameters using exported types const startTime = new Date('2024-01-01T09:00:00Z') @@ -186,35 +184,36 @@ describe('Index Exports', () => { it('should export all expected components', () => { // Verify all main exports are present expect(Scheduler).toBeDefined() - expect(createScheduler).toBeDefined() - expect(typeof createScheduler).toBe('function') + expect(AvailabilityScheduler).toBeDefined() + expect(typeof Scheduler).toBe('function') + expect(typeof AvailabilityScheduler).toBe('function') }) - it('should maintain consistent API across export methods', () => { - const directScheduler = new Scheduler() - const convenienceScheduler = createScheduler() + it('should maintain consistent API for Scheduler', () => { + const scheduler1 = new Scheduler() + const scheduler2 = new Scheduler() // Both should have the same methods - expect(typeof directScheduler.findAvailableSlots).toBe('function') - expect(typeof convenienceScheduler.findAvailableSlots).toBe('function') + expect(typeof scheduler1.findAvailableSlots).toBe('function') + expect(typeof scheduler2.findAvailableSlots).toBe('function') - expect(typeof directScheduler.addBusyTimes).toBe('function') - expect(typeof convenienceScheduler.addBusyTimes).toBe('function') + expect(typeof scheduler1.addBusyTimes).toBe('function') + expect(typeof scheduler2.addBusyTimes).toBe('function') - expect(typeof directScheduler.clearBusyTimes).toBe('function') - expect(typeof convenienceScheduler.clearBusyTimes).toBe('function') + expect(typeof scheduler1.clearBusyTimes).toBe('function') + expect(typeof scheduler2.clearBusyTimes).toBe('function') - expect(typeof directScheduler.getBusyTimes).toBe('function') - expect(typeof convenienceScheduler.getBusyTimes).toBe('function') + expect(typeof scheduler1.getBusyTimes).toBe('function') + expect(typeof scheduler2.getBusyTimes).toBe('function') }) }) - describe('createAvailabilityScheduler convenience function', () => { + describe('AvailabilityScheduler instantiation (as per README)', () => { it('should create an AvailabilityScheduler instance', () => { const availability: WeeklyAvailability = { schedules: [{ days: ['monday'], start: '09:00', end: '17:00' }], } - const scheduler = createAvailabilityScheduler(availability, 'America/New_York') + const scheduler = new AvailabilityScheduler(availability, 'America/New_York') expect(scheduler).toBeInstanceOf(AvailabilityScheduler) expect(scheduler.getAvailability()).toEqual(availability) }) @@ -223,7 +222,7 @@ describe('Index Exports', () => { const availability: WeeklyAvailability = { schedules: [{ days: ['tuesday'], start: 540, end: 1020 }], // 9:00 (540 min) to 17:00 (1020 min) } - const scheduler = createAvailabilityScheduler(availability, 'UTC') + const scheduler = new AvailabilityScheduler(availability, 'UTC') const slots = scheduler.findAvailableSlots( new Date('2024-01-02T08:00:00Z'), // Tuesday new Date('2024-01-02T18:00:00Z'), @@ -237,7 +236,7 @@ describe('Index Exports', () => { const availability: WeeklyAvailability = { schedules: [{ days: ['monday'], start: '09:00', end: '10:00' }], } - const scheduler = createAvailabilityScheduler(availability, 'America/New_York') + const scheduler = new AvailabilityScheduler(availability, 'America/New_York') const slots = scheduler.findAvailableSlots( new Date('2024-01-15T00:00:00Z'), // Monday UTC new Date('2024-01-15T23:59:00Z'), From d91eb703190f58cdaa0b04a7a0528b865cc7cee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 15:59:31 +0200 Subject: [PATCH 07/12] simplify formatting --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index dbf94dc..d9532e2 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ "test": "bun test", "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text", "typecheck": "tsc --noEmit", - "format": "biome format", - "format:fix": "biome format --write", + "format": "biome format --write", "lint": "biome lint", "lint:fix": "biome lint --write", "lint:fix:all": "biome lint --write --unsafe", From 1b984abc956ed9c6ee42181ac14ca66eb2c8b2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 16:09:54 +0200 Subject: [PATCH 08/12] align tests better --- tests/index.test.ts | 88 +++------------------------------------------ 1 file changed, 4 insertions(+), 84 deletions(-) diff --git a/tests/index.test.ts b/tests/index.test.ts index 91739bd..6f3691c 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -39,35 +39,13 @@ describe('Index Exports', () => { }) }) - describe('Scheduler class export', () => { - it('should export Scheduler class', () => { - expect(Scheduler).toBeDefined() - expect(typeof Scheduler).toBe('function') - }) - - it('should create Scheduler instances', () => { - const scheduler = new Scheduler() - expect(scheduler).toBeInstanceOf(Scheduler) - expect(typeof scheduler.findAvailableSlots).toBe('function') - expect(typeof scheduler.addBusyTimes).toBe('function') - expect(typeof scheduler.clearBusyTimes).toBe('function') - expect(typeof scheduler.getBusyTimes).toBe('function') - }) - }) - - describe('Scheduler instantiation (as per README)', () => { + describe('Scheduler constructor behavior', () => { it('should create a Scheduler instance with no arguments', () => { const scheduler = new Scheduler() expect(scheduler).toBeInstanceOf(Scheduler) expect(scheduler.getBusyTimes()).toEqual([]) }) - it('should create a Scheduler instance with empty busy times', () => { - const scheduler = new Scheduler([]) - expect(scheduler).toBeInstanceOf(Scheduler) - expect(scheduler.getBusyTimes()).toEqual([]) - }) - it('should create a Scheduler instance with provided busy times', () => { const busyTimes: BusyTime[] = [ { start: new Date('2024-01-01T10:00:00Z'), end: new Date('2024-01-01T11:00:00Z') }, @@ -79,44 +57,6 @@ describe('Index Exports', () => { expect(scheduler.getBusyTimes()).toEqual(busyTimes) }) - it('should create a functional scheduler instance', () => { - const busyTimes: BusyTime[] = [ - { start: new Date('2024-01-01T12:00:00Z'), end: new Date('2024-01-01T13:00:00Z') }, - ] - - const scheduler = new Scheduler(busyTimes) - - const startTime = new Date('2024-01-01T09:00:00Z') - const endTime = new Date('2024-01-01T17:00:00Z') - const options: SchedulingOptions = { - slotDuration: 60, - slotSplit: 60, - padding: 0, - offset: 0, - } - - const slots = scheduler.findAvailableSlots(startTime, endTime, options) - expect(Array.isArray(slots)).toBe(true) - expect(slots.length).toBeGreaterThan(0) - - // Verify slots don't conflict with busy time - slots.forEach(slot => { - expect( - slot.end.getTime() <= new Date('2024-01-01T12:00:00Z').getTime() || - slot.start.getTime() >= new Date('2024-01-01T13:00:00Z').getTime() - ).toBe(true) - }) - }) - - it('should return different instances on multiple calls', () => { - const scheduler1 = new Scheduler() - const scheduler2 = new Scheduler() - - expect(scheduler1).not.toBe(scheduler2) - expect(scheduler1).toBeInstanceOf(Scheduler) - expect(scheduler2).toBeInstanceOf(Scheduler) - }) - it('should not share state between instances', () => { const scheduler1 = new Scheduler() const scheduler2 = new Scheduler() @@ -181,34 +121,14 @@ describe('Index Exports', () => { }) describe('API surface validation', () => { - it('should export all expected components', () => { - // Verify all main exports are present + it('should export main classes', () => { expect(Scheduler).toBeDefined() - expect(AvailabilityScheduler).toBeDefined() expect(typeof Scheduler).toBe('function') - expect(typeof AvailabilityScheduler).toBe('function') - }) - - it('should maintain consistent API for Scheduler', () => { - const scheduler1 = new Scheduler() - const scheduler2 = new Scheduler() - - // Both should have the same methods - expect(typeof scheduler1.findAvailableSlots).toBe('function') - expect(typeof scheduler2.findAvailableSlots).toBe('function') - - expect(typeof scheduler1.addBusyTimes).toBe('function') - expect(typeof scheduler2.addBusyTimes).toBe('function') - - expect(typeof scheduler1.clearBusyTimes).toBe('function') - expect(typeof scheduler2.clearBusyTimes).toBe('function') - - expect(typeof scheduler1.getBusyTimes).toBe('function') - expect(typeof scheduler2.getBusyTimes).toBe('function') + expect(AvailabilityScheduler).toBeDefined() }) }) - describe('AvailabilityScheduler instantiation (as per README)', () => { + describe('AvailabilityScheduler constructor usage', () => { it('should create an AvailabilityScheduler instance', () => { const availability: WeeklyAvailability = { schedules: [{ days: ['monday'], start: '09:00', end: '17:00' }], From 5e761ffc75bbc3487be4940001042e16486d504c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 16:16:19 +0200 Subject: [PATCH 09/12] align docs --- docs/performance.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/performance.md b/docs/performance.md index 36f714d..a76fae5 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -113,12 +113,12 @@ scheduler.addBusyTimes([meeting1, meeting2, meeting3]) ```typescript // ❌ Inefficient: Creating new schedulers function findSlots(busyTimes, start, end, options) { - const scheduler = createScheduler(busyTimes) + const scheduler = new Scheduler(busyTimes) return scheduler.findAvailableSlots(start, end, options) } // ✅ Efficient: Reuse existing scheduler -const scheduler = createScheduler() +const scheduler = new Scheduler() function findSlots(busyTimes, start, end, options) { scheduler.clearBusyTimes() @@ -186,7 +186,7 @@ function addBusinessHoursConstraints(scheduler) { ```typescript class PerformantScheduler { - private scheduler = createScheduler() + private scheduler = new Scheduler() private maxBusyTimes = 1000 addBusyTimes(busyTimes: BusyTime[]) { @@ -251,7 +251,7 @@ monitorMemory('After scheduling') import { performance } from 'perf_hooks' function benchmarkScheduling() { - const scheduler = createScheduler() + const scheduler = new Scheduler() // Generate test data const busyTimes = generateRandomBusyTimes(1000) @@ -299,7 +299,7 @@ class PartitionedScheduler { const key = this.getPartitionKey(busyTime.start) if (!this.schedulers.has(key)) { - this.schedulers.set(key, createScheduler()) + this.schedulers.set(key, new Scheduler()) } this.schedulers.get(key)!.addBusyTimes([busyTime]) @@ -328,7 +328,7 @@ class PartitionedScheduler { ```typescript class CachedScheduler { - private scheduler = createScheduler() + private scheduler = new Scheduler() private cache = new Map() findAvailableSlots(start: Date, end: Date, options: SchedulingOptions) { @@ -372,12 +372,12 @@ class CachedScheduler { ```typescript // ❌ Recreating scheduler repeatedly meetings.forEach(meeting => { - const scheduler = createScheduler([meeting]) + const scheduler = new Scheduler([meeting]) // ... use scheduler }) // ✅ Single scheduler instance -const scheduler = createScheduler(meetings) +const scheduler = new Scheduler(meetings) ``` ### 3. Large Time Ranges From 047a20a4e1314f711a0def844ae34bb74ec50401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 16:16:35 +0200 Subject: [PATCH 10/12] create docs readme --- docs/README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..6a47f30 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,31 @@ +# Scheduling SDK Docs + +Welcome to the Scheduling SDK documentation. Start here to explore concepts, APIs, and practical recipes. + +## Start Here + +- [Getting Started](getting-started.md) — installation and a 5‑minute tour +- [Core Concepts](core-concepts.md) — daily windows, K‑overlaps, alignment, and more +- [Availability Scheduler](availability-scheduler.md) — weekly availability patterns and business hours +- [API Reference](api-reference.md) — full signatures and validation rules + +## Practical Guides + +- [Examples / Recipes](examples.md) — copy‑paste scenarios for common use cases +- [Performance](performance.md) — benchmarks and optimization strategies + +## Contributing + +- [Contributing Guide](contributing.md) + +## When to use what + +- Use `Scheduler` when you have busy times and need to find open slots between them. +- Use `AvailabilityScheduler` when you have a recurring weekly availability (e.g., business hours) and also need to apply busy times and daily windows. + +## Quick Links (anchors) + +- [Daily Windows](core-concepts.md#daily-windows) +- [K-overlaps](core-concepts.md#k-overlaps) +- [Availability Scheduler](availability-scheduler.md) +- [weeklyAvailabilityToBusyTimes](api-reference.md#weeklyavailabilitytobusytimes) From 14ba315c10ad9e8fe7d373b17bd9a1ab74288969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 16:36:57 +0200 Subject: [PATCH 11/12] update docs --- README.md | 4 +- docs/README.md | 2 +- docs/api-reference.md | 2 + docs/availability-api.md | 612 ------------------------------- docs/availability-scheduler.md | 347 ++++++++++++++++++ docs/core-concepts.md | 4 +- docs/getting-started.md | 6 +- docs/{examples.md => recipes.md} | 58 +-- 8 files changed, 366 insertions(+), 669 deletions(-) delete mode 100644 docs/availability-api.md create mode 100644 docs/availability-scheduler.md rename docs/{examples.md => recipes.md} (92%) diff --git a/README.md b/README.md index 4d22ba9..7baa402 100644 --- a/README.md +++ b/README.md @@ -271,8 +271,8 @@ interface SchedulingOptions { - **📖 [Getting Started](docs/getting-started.md)** - Installation and basic usage - **📋 [API Reference](docs/api-reference.md)** - Complete API documentation - **🧠 [Core Concepts](docs/core-concepts.md)** - Understanding scheduling concepts -- **⏰ [Availability API](docs/availability-api.md)** - Weekly availability patterns and scheduling -- **💡 [Examples](docs/examples.md)** - Practical usage examples +- **⏰ [Availability Scheduler](docs/availability-scheduler.md)** - Weekly availability patterns and scheduling +- **💡 [Recipes](docs/recipes.md)** - Practical usage examples - **⚡ [Performance Guide](docs/performance.md)** - Optimization and benchmarks - **🤝 [Contributing](docs/contributing.md)** - Development and contribution guidelines diff --git a/docs/README.md b/docs/README.md index 6a47f30..443ed6b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ Welcome to the Scheduling SDK documentation. Start here to explore concepts, API ## Practical Guides -- [Examples / Recipes](examples.md) — copy‑paste scenarios for common use cases +- [Recipes](recipes.md) — copy‑paste scenarios for common use cases - [Performance](performance.md) — benchmarks and optimization strategies ## Contributing diff --git a/docs/api-reference.md b/docs/api-reference.md index 7015611..62f86bb 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,5 +1,7 @@ # API Reference +Back: [Availability Scheduler](availability-scheduler.md) • Next: [Recipes](recipes.md) + ## Types ### Core Types diff --git a/docs/availability-api.md b/docs/availability-api.md deleted file mode 100644 index 1620f12..0000000 --- a/docs/availability-api.md +++ /dev/null @@ -1,612 +0,0 @@ -# Weekly Availability API Documentation - -The Weekly Availability API allows you to define recurring weekly schedules that specify when time slots are available for scheduling. This is perfect for businesses, professionals, or services that operate on predictable weekly patterns. - -## Table of Contents - -- [Core Concepts](#core-concepts) -- [Types and Interfaces](#types-and-interfaces) -- [AvailabilityScheduler Class](#availabilityscheduler-class) -- [Helper Functions](#helper-functions) -- [Usage Examples](#usage-examples) -- [Best Practices](#best-practices) -- [Common Pitfalls](#common-pitfalls) -- [Error Handling](#error-handling) - -## Core Concepts - -### Weekly Availability Pattern - -The availability system works by defining **available time periods** rather than busy times. The system automatically converts these available periods into busy times that block out all other times. - -### Implicit Breaks - -Instead of explicitly defining breaks, you create breaks by defining multiple separate time blocks for the same day: - -```typescript -// Creates a lunch break on Monday from 12:00-13:00 -{ - schedules: [ - { days: ['monday'], start: '09:00', end: '12:00' }, // Morning - { days: ['monday'], start: '13:00', end: '17:00' }, // Afternoon - ] -} -``` - -### Week-Based Processing - -All availability processing is done on a weekly basis starting from Monday. You must provide a Monday date when converting availability to busy times. - -## Types and Interfaces - -### `DayOfWeek` - -```typescript -type DayOfWeek = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday' -``` - -Represents days of the week as lowercase strings. - -### `DaySchedule` - -```typescript -interface DaySchedule { - days: DayOfWeek[] // Array of days this schedule applies to - start: string // Start time in HH:mm format (24-hour) - end: string // End time in HH:mm format (24-hour) -} -``` - -**Key Requirements:** - -- `days`: Must contain at least one valid day -- `start`/`end`: Must be in `HH:mm` format (e.g., "09:00", "14:30") -- `start` must be before `end` -- Times are in 24-hour format (00:00 to 23:59) - -### `WeeklyAvailability` - -```typescript -interface WeeklyAvailability { - schedules: DaySchedule[] // Array of availability schedules -} -``` - -**Key Requirements:** - -- `schedules`: Must contain at least one schedule - -## AvailabilityScheduler Class - -The main class for working with weekly availability patterns. - -### Constructor - -```typescript -constructor(availability?: WeeklyAvailability, timezone?: string, existingBusyTimes: BusyTime[] = []) -``` - -Creates a new availability scheduler with optional initial availability and existing busy times. - -**Parameters:** - -- `availability` (optional): The weekly availability pattern -- `timezone` (optional): IANA timezone identifier used for availability processing and daily window fallback -- `existingBusyTimes` (optional): Array of existing busy times to include - -**Example:** - -```typescript -const availability = { - schedules: [{ days: ['monday', 'tuesday'], start: '09:00', end: '17:00' }], -} - -const scheduler = new AvailabilityScheduler(availability, 'America/New_York') -``` - -### Methods - -#### `setAvailability(availability: WeeklyAvailability): void` - -Sets or updates the weekly availability pattern. - -**⚠️ Important:** This completely replaces the existing availability pattern. - -```typescript -scheduler.setAvailability({ - schedules: [{ days: ['monday', 'wednesday', 'friday'], start: '10:00', end: '16:00' }], -}) -``` - -#### `getAvailability(): WeeklyAvailability | undefined` - -Returns the current availability pattern or `undefined` if none is set. - -```typescript -const currentAvailability = scheduler.getAvailability() -if (currentAvailability) { - console.log(`Found ${currentAvailability.schedules.length} schedules`) -} -``` - -#### `addBusyTime(busyTime: BusyTime): void` - -Adds a single busy time that will be combined with availability-based busy times. - -**Use Case:** One-off appointments, meetings, or exceptions to the regular schedule. - -```typescript -// Block out a specific appointment -scheduler.addBusyTime({ - start: new Date('2024-01-15T14:00:00Z'), - end: new Date('2024-01-15T15:00:00Z'), -}) -``` - -#### `addBusyTimes(busyTimes: BusyTime[]): void` - -Adds multiple busy times at once. - -```typescript -const appointments = [ - { start: new Date('2024-01-15T10:00:00Z'), end: new Date('2024-01-15T11:00:00Z') }, - { start: new Date('2024-01-15T15:00:00Z'), end: new Date('2024-01-15T16:00:00Z') }, -] -scheduler.addBusyTimes(appointments) -``` - -#### `clearBusyTimes(): void` - -Removes all manually added busy times. **Does not affect availability-based busy times.** - -```typescript -scheduler.clearBusyTimes() // Only removes manually added busy times -``` - -#### `getBusyTimes(): BusyTime[]` - -Returns all manually added busy times (not including availability-based busy times). - -```typescript -const manualBusyTimes = scheduler.getBusyTimes() -console.log(`${manualBusyTimes.length} manual busy times`) -``` - -#### `findAvailableSlots(startTime: Date, endTime: Date, options: SchedulingOptions): TimeSlot[]` - -Finds available time slots within the specified time range, considering both availability patterns and manually added busy times. - -**Parameters:** - -- `startTime`: Start of the search range -- `endTime`: End of the search range -- `options`: Slot generation options (duration, split, offset, padding) - -**Returns:** Array of available time slots - -**⚠️ Important Behavior:** - -- If no availability pattern is set, behaves like the standard Scheduler -- If availability is set, only returns slots within available periods -- Combines availability restrictions with manually added busy times - -```typescript -const slots = scheduler.findAvailableSlots(new Date('2024-01-15T08:00:00Z'), new Date('2024-01-15T18:00:00Z'), { - slotDuration: 60, // 60-minute slots - slotSplit: 60, // No overlap - padding: 15, // 15-minute buffer around busy times -}) -``` - -## Helper Functions - -### `weeklyAvailabilityToBusyTimes(availability: WeeklyAvailability, weekStart: Date, timezone?: string): BusyTime[]` - -Converts a weekly availability pattern into busy times for a specific week. - -**Parameters:** - -- `availability`: The weekly availability pattern -- `weekStart`: **Must be a Monday** (Date.getDay() === 1) -- `timezone` (optional): IANA timezone identifier used to interpret availability times. Falls back to `process.env.SCHEDULING_TIMEZONE` or `UTC`. - -**Returns:** Array of busy times representing unavailable periods - -**⚠️ Critical Requirements:** - -- `weekStart` MUST be a Monday, or the function throws an error -- The function generates busy times for the entire week (7 days) -- Available periods become gaps in the busy times - -**Example:** - -```typescript -const mondayDate = new Date('2024-01-01T00:00:00Z') // Must be Monday -const availability = { - schedules: [{ days: ['monday'], start: '09:00', end: '17:00' }], -} - -const busyTimes = weeklyAvailabilityToBusyTimes(availability, mondayDate) -// Returns busy times for: -// - Monday 00:00-09:00 and 17:00-23:59 -// - Tuesday-Sunday all day -``` - -### `validateWeeklyAvailability(availability?: WeeklyAvailability): void` - -Validates a weekly availability object and throws descriptive errors if invalid. - -**Validation Rules:** - -- `availability` can be undefined (returns without error) -- Must be an object (not null, string, number, etc.) -- Must have a `schedules` array with at least one schedule -- Each schedule must have valid `days`, `start`, and `end` -- Times must be in HH:mm format and start must be before end -- No overlapping schedules on the same day -- Optional `timezone` must be valid IANA format - -**Example:** - -```typescript -try { - validateWeeklyAvailability(availability) - console.log('Availability is valid') -} catch (error) { - console.error('Invalid availability:', error.message) -} -``` - -## Usage Examples - -### Example 1: Standard Business Hours - -```typescript -import { AvailabilityScheduler } from './scheduling-sdk' - -const businessHours = { - schedules: [ - { - days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], - start: '09:00', - end: '17:00', - }, - ], -} - -const scheduler = new AvailabilityScheduler(businessHours, 'America/New_York') - -// Find 1-hour appointment slots for this week -const slots = scheduler.findAvailableSlots(new Date('2024-01-15T00:00:00Z'), new Date('2024-01-19T23:59:59Z'), { - slotDuration: 60, - slotSplit: 60, -}) -``` - -### Example 2: Different Hours Per Day - -```typescript -const doctorSchedule = { - schedules: [ - { days: ['monday', 'wednesday', 'friday'], start: '08:00', end: '16:00' }, - { days: ['tuesday', 'thursday'], start: '12:00', end: '20:00' }, - { days: ['saturday'], start: '09:00', end: '13:00' }, - ], -} - -const scheduler = new AvailabilityScheduler(doctorSchedule) -``` - -### Example 3: Lunch Breaks - -```typescript -const scheduleWithLunch = { - schedules: [ - // Morning sessions - { days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], start: '09:00', end: '12:00' }, - // Afternoon sessions (lunch break 12:00-13:00) - { days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], start: '13:00', end: '17:00' }, - ], -} -``` - -### Example 4: Adding Exceptions - -```typescript -const scheduler = new AvailabilityScheduler(regularSchedule) - -// Add a one-off meeting -scheduler.addBusyTime({ - start: new Date('2024-01-15T14:00:00Z'), - end: new Date('2024-01-15T15:30:00Z'), -}) - -// Add vacation days -const vacationDays = [ - { start: new Date('2024-01-20T00:00:00Z'), end: new Date('2024-01-21T00:00:00Z') }, - { start: new Date('2024-01-21T00:00:00Z'), end: new Date('2024-01-22T00:00:00Z') }, -] -scheduler.addBusyTimes(vacationDays) -``` - -### Example 5: Dynamic Schedule Updates - -```typescript -const scheduler = new AvailabilityScheduler() - -// Start with summer hours -scheduler.setAvailability({ - schedules: [ - { days: ['monday', 'tuesday', 'wednesday', 'thursday'], start: '08:00', end: '16:00' }, - { days: ['friday'], start: '08:00', end: '12:00' }, - ], -}) - -// Later, switch to winter hours -scheduler.setAvailability({ - schedules: [{ days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], start: '09:00', end: '17:00' }], -}) -``` - -## Best Practices - -### 1. Time Format Consistency - -```typescript -// ✅ Good - consistent 24-hour format -{ start: '09:00', end: '17:00' } - -// ❌ Bad - inconsistent formatting -{ start: '9:00', end: '5:00 PM' } // Will throw validation error -``` - -### 2. Handling Timezones - -```typescript -// ✅ Specify timezone at the scheduler level -const availability = { schedules: [...] } -const scheduler = new AvailabilityScheduler(availability, 'America/Los_Angeles') - -// ✅ Or specify timezone when converting availability for a week -const localMondayStart = new Date('2024-01-01T00:00:00Z') // Monday UTC -const busyTimes = weeklyAvailabilityToBusyTimes(availability, localMondayStart, 'America/Los_Angeles') -``` - -### 3. Break Management - -```typescript -// ✅ Good - create breaks with separate time blocks -{ - schedules: [ - { days: ['monday'], start: '09:00', end: '12:00' }, // Morning - { days: ['monday'], start: '13:30', end: '17:00' }, // Afternoon (90-min lunch) - ] -} - -// ❌ Avoid - trying to specify breaks as busy times with availability -// (Use addBusyTime for exceptions, not regular breaks) -``` - -### 4. Validation Early and Often - -```typescript -// ✅ Good - validate before using -try { - validateWeeklyAvailability(userProvidedAvailability) - const scheduler = new AvailabilityScheduler(userProvidedAvailability) -} catch (error) { - // Handle validation error gracefully - showErrorToUser(error.message) - return -} -``` - -### 5. Week Boundary Awareness - -```typescript -// ✅ Good - always use Monday for week start -const mondayStart = getMondayOfWeek(someDate) -const busyTimes = weeklyAvailabilityToBusyTimes(availability, mondayStart) - -// ❌ Bad - using arbitrary dates -const today = new Date() // Might not be Monday! -weeklyAvailabilityToBusyTimes(availability, today) // Throws error -``` - -## Common Pitfalls - -### 1. **Non-Monday Week Start** - -```typescript -// ❌ Error: This will throw if today is not Monday -const today = new Date() -weeklyAvailabilityToBusyTimes(availability, today) - -// ✅ Correct: Always find the Monday of the week -function getMondayOfWeek(date: Date): Date { - const day = date.getDay() - const diff = day === 0 ? -6 : 1 - day - const monday = new Date(date) - monday.setDate(date.getDate() + diff) - return new Date(monday.getFullYear(), monday.getMonth(), monday.getDate()) -} -``` - -### 2. **Time Format Mistakes** - -```typescript -// ❌ Invalid formats that will cause validation errors -{ start: '9:00', end: '17:00' } // Missing leading zero -{ start: '09:00 AM', end: '5:00 PM' } // 12-hour format -{ start: '9', end: '17' } // Missing minutes -{ start: '25:00', end: '26:00' } // Invalid hours - -// ✅ Correct format -{ start: '09:00', end: '17:00' } -``` - -### 3. **Overlapping Schedules** - -```typescript -// ❌ This will fail validation - schedules overlap 16:00-17:00 -{ - schedules: [ - { days: ['monday'], start: '09:00', end: '17:00' }, - { days: ['monday'], start: '16:00', end: '20:00' }, // Overlaps! - ] -} - -// ✅ Correct - adjacent schedules are fine -{ - schedules: [ - { days: ['monday'], start: '09:00', end: '17:00' }, - { days: ['monday'], start: '17:00', end: '20:00' }, // Starts when other ends - ] -} -``` - -### 4. **Misunderstanding Busy Times vs Availability** - -```typescript -// ❌ Common mistake - thinking availability defines busy times -const scheduler = new AvailabilityScheduler({ - schedules: [ - { days: ['monday'], start: '12:00', end: '13:00' }, // User thinks this is lunch break - ], -}) -// This actually makes ONLY 12:00-13:00 available, everything else is busy! - -// ✅ Correct - availability defines when you're available -const scheduler = new AvailabilityScheduler({ - schedules: [ - { days: ['monday'], start: '09:00', end: '12:00' }, // Morning availability - { days: ['monday'], start: '13:00', end: '17:00' }, // Afternoon availability - ], -}) -// Lunch break (12:00-13:00) is automatically created by the gap -``` - -### 5. **Forgetting to Handle No Availability** - -```typescript -// ❌ Not handling the case where no availability is set -const scheduler = new AvailabilityScheduler() // No availability -const slots = scheduler.findAvailableSlots(start, end, options) -// This works (falls back to normal scheduling) but might not be intended - -// ✅ Explicit handling -const scheduler = new AvailabilityScheduler() -if (!scheduler.getAvailability()) { - throw new Error('Please set availability before finding slots') -} -``` - -## Error Handling - -### Validation Errors - -All validation errors include descriptive messages indicating exactly what's wrong: - -```typescript -try { - validateWeeklyAvailability(availability) -} catch (error) { - // Examples of error messages: - // "Schedule at index 0: start time (17:00) must be before end time (09:00)" - // "Schedule at index 1: invalid day 'tuesday'. Valid days: monday, tuesday, ..." - // "Overlapping schedules found for monday: schedule 1 (12:00-17:00) overlaps with schedule 0" - // "Availability.timezone must be a valid IANA timezone identifier" - - console.error('Validation failed:', error.message) -} -``` - -### Runtime Errors - -```typescript -try { - const tuesday = new Date('2024-01-02') // Tuesday - weeklyAvailabilityToBusyTimes(availability, tuesday) -} catch (error) { - // "weekStart must be a Monday (getDay() === 1)" - console.error('Runtime error:', error.message) -} -``` - -### Best Practice Error Handling - -```typescript -function createSchedulerSafely(availability: WeeklyAvailability): AvailabilityScheduler | null { - try { - validateWeeklyAvailability(availability) - return new AvailabilityScheduler(availability) - } catch (error) { - console.error('Failed to create scheduler:', error.message) - - // Log detailed error for debugging - if (error.message.includes('overlapping')) { - console.log('Tip: Check for overlapping time periods on the same day') - } else if (error.message.includes('time format')) { - console.log('Tip: Use HH:mm format (e.g., "09:00", "14:30")') - } - - return null - } -} -``` - -## Performance Considerations - -### 1. **Week-by-Week Processing** - -The availability system processes one week at a time. For multi-week scheduling: - -```typescript -// ✅ Efficient for single week -const thisWeek = weeklyAvailabilityToBusyTimes(availability, mondayStart) - -// ⚠️ For multiple weeks, call separately for each week -const week1 = weeklyAvailabilityToBusyTimes(availability, monday1) -const week2 = weeklyAvailabilityToBusyTimes(availability, monday2) -const allBusyTimes = [...week1, ...week2] -``` - -### 2. **Caching Busy Times** - -If you're using the same availability pattern repeatedly: - -```typescript -// ✅ Cache converted busy times for repeated use -const cache = new Map() - -function getCachedBusyTimes(availability: WeeklyAvailability, weekStart: Date): BusyTime[] { - const key = `${weekStart.toISOString()}-${JSON.stringify(availability)}` - - if (!cache.has(key)) { - cache.set(key, weeklyAvailabilityToBusyTimes(availability, weekStart)) - } - - return cache.get(key)! -} -``` - -### 3. **Minimize Scheduler Recreation** - -```typescript -// ✅ Reuse scheduler instance -const scheduler = new AvailabilityScheduler(availability) - -// Add temporary busy times for different queries -scheduler.addBusyTime(appointment1) -const slots1 = scheduler.findAvailableSlots(...) - -scheduler.clearBusyTimes() -scheduler.addBusyTime(appointment2) -const slots2 = scheduler.findAvailableSlots(...) - -// ❌ Avoid recreating for each query -// const scheduler1 = new AvailabilityScheduler(availability) -// const scheduler2 = new AvailabilityScheduler(availability) -``` - -This documentation provides comprehensive guidance for using the Weekly Availability API effectively while avoiding common mistakes and performance issues. diff --git a/docs/availability-scheduler.md b/docs/availability-scheduler.md new file mode 100644 index 0000000..35de981 --- /dev/null +++ b/docs/availability-scheduler.md @@ -0,0 +1,347 @@ +# Availability Scheduler + +Back: [Core Concepts](core-concepts.md) • Next: [API Reference](api-reference.md) + +Define recurring weekly availability patterns (e.g., business hours) and find available slots that respect both those patterns and manually-added busy times. This guide covers the `AvailabilityScheduler` class and related helpers. + +## Table of Contents + +- Core Concepts +- Types and Interfaces +- AvailabilityScheduler Class +- Helper Functions +- Usage Examples +- Best Practices +- Common Pitfalls +- Error Handling + +## Core Concepts + +### Weekly Availability Pattern + +Define available time periods per day (not busy times). The system converts these available periods into busy times so everything outside is considered unavailable. + +### Implicit Breaks + +Create breaks by using multiple time blocks for the same day: + +```typescript +// Creates a lunch break on Monday from 12:00-13:00 +{ + schedules: [ + { days: ['monday'], start: '09:00', end: '12:00' }, // Morning + { days: ['monday'], start: '13:00', end: '17:00' }, // Afternoon + ] +} +``` + +### Week-Based Processing + +All availability processing is done on a weekly basis starting from Monday. You must provide a Monday date when converting availability to busy times. + +## Types and Interfaces + +### `DayOfWeek` + +```typescript +type DayOfWeek = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday' +``` + +### `DaySchedule` + +```typescript +interface DaySchedule { + days: DayOfWeek[] + start: string | number // 'HH:mm' or minutes since midnight + end: string | number // 'HH:mm' or minutes since midnight +} +``` + +Key requirements: + +- `days`: Must contain at least one valid day +- `start`/`end`: Must be valid times +- `start` must be before `end` + +### `WeeklyAvailability` + +```typescript +interface WeeklyAvailability { + schedules: DaySchedule[] +} +``` + +Key requirement: + +- `schedules`: Must contain at least one schedule + +## AvailabilityScheduler Class + +The main class for working with weekly availability patterns. + +### Constructor + +```typescript +constructor(availability?: WeeklyAvailability, timezone?: string, existingBusyTimes: BusyTime[] = []) +``` + +Parameters: + +- `availability` (optional): The weekly availability pattern +- `timezone` (optional): IANA timezone identifier used for availability processing and daily window fallback +- `existingBusyTimes` (optional): Array of existing busy times to include + +Example: + +```typescript +import { AvailabilityScheduler } from 'scheduling-sdk' + +const availability = { + schedules: [{ days: ['monday', 'tuesday'], start: '09:00', end: '17:00' }], +} + +const scheduler = new AvailabilityScheduler(availability, 'America/New_York') +``` + +### Methods + +- `setAvailability(availability: WeeklyAvailability): void` — replaces the existing availability pattern. +- `getAvailability(): WeeklyAvailability | undefined` — returns the current availability pattern. +- `addBusyTime(busyTime: BusyTime): void` +- `addBusyTimes(busyTimes: BusyTime[]): void` +- `clearBusyTimes(): void` +- `getBusyTimes(): BusyTime[]` +- `findAvailableSlots(startTime: Date, endTime: Date, options: SchedulingOptions): TimeSlot[]` + +Find available time slots within the specified time range, considering both availability patterns and manually added busy times. + +Important behavior: + +- If no availability pattern is set, behaves like the standard Scheduler +- If availability is set, only returns slots within available periods +- Combines availability restrictions with manually added busy times + +```typescript +const slots = scheduler.findAvailableSlots( + new Date('2024-01-15T08:00:00Z'), + new Date('2024-01-15T18:00:00Z'), + { slotDuration: 60, slotSplit: 60, padding: 15 } +) +``` + +## Helper Functions + +### `weeklyAvailabilityToBusyTimes(availability: WeeklyAvailability, weekStart: Date, timezone?: string): BusyTime[]` + +Converts a weekly availability pattern into busy times for a specific week. + +Parameters: + +- `availability`: The weekly availability pattern to convert +- `weekStart`: Must be a Monday (`Date.getDay() === 1`) +- `timezone` (optional): IANA timezone used to interpret availability times. Falls back to `process.env.SCHEDULING_TIMEZONE` or `UTC`. + +Returns: Array of busy times representing unavailable periods. + +Critical requirements: + +- `weekStart` MUST be a Monday, or the function throws an error +- Generates busy times for the entire week (7 days) +- Available periods become gaps in the busy times + +```typescript +const mondayDate = new Date('2024-01-01T00:00:00Z') // Must be Monday +const availability = { + schedules: [{ days: ['monday'], start: '09:00', end: '17:00' }], +} + +const busyTimes = weeklyAvailabilityToBusyTimes(availability, mondayDate, 'America/New_York') +``` + +### `validateWeeklyAvailability(availability?: WeeklyAvailability): void` + +Validates a weekly availability object and throws descriptive errors if invalid. + +Validation rules: + +- `availability` can be undefined (returns without error) +- Must be an object (not null, string, number, etc.) +- Must have a `schedules` array with at least one schedule +- Each schedule must have valid `days`, `start`, and `end` +- Times must be valid and start must be before end +- No overlapping schedules on the same day + +## Usage Examples + +### Example 1: Standard Business Hours + +```typescript +import { AvailabilityScheduler } from 'scheduling-sdk' + +const businessHours = { + schedules: [ + { + days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], + start: '09:00', + end: '17:00', + }, + ], +} + +const scheduler = new AvailabilityScheduler(businessHours, 'America/New_York') + +const slots = scheduler.findAvailableSlots( + new Date('2024-01-15T00:00:00Z'), + new Date('2024-01-19T23:59:59Z'), + { slotDuration: 60, slotSplit: 60 } +) +``` + +### Example 2: Different Hours Per Day + +```typescript +const doctorSchedule = { + schedules: [ + { days: ['monday', 'wednesday', 'friday'], start: '08:00', end: '16:00' }, + { days: ['tuesday', 'thursday'], start: '12:00', end: '20:00' }, + { days: ['saturday'], start: '09:00', end: '13:00' }, + ], +} + +const scheduler = new AvailabilityScheduler(doctorSchedule) +``` + +### Example 3: Lunch Breaks + +```typescript +const scheduleWithLunch = { + schedules: [ + { days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], start: '09:00', end: '12:00' }, + { days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], start: '13:00', end: '17:00' }, + ], +} +``` + +### Example 4: Adding Exceptions + +```typescript +const scheduler = new AvailabilityScheduler(regularSchedule) + +scheduler.addBusyTime({ + start: new Date('2024-01-15T14:00:00Z'), + end: new Date('2024-01-15T15:30:00Z'), +}) + +const vacationDays = [ + { start: new Date('2024-01-20T00:00:00Z'), end: new Date('2024-01-21T00:00:00Z') }, + { start: new Date('2024-01-21T00:00:00Z'), end: new Date('2024-01-22T00:00:00Z') }, +] + +scheduler.addBusyTimes(vacationDays) +``` + +### Example 5: Dynamic Schedule Updates + +```typescript +const scheduler = new AvailabilityScheduler() + +// Start with summer hours +scheduler.setAvailability({ + schedules: [ + { days: ['monday', 'tuesday', 'wednesday', 'thursday'], start: '08:00', end: '16:00' }, + { days: ['friday'], start: '08:00', end: '12:00' }, + ], +}) + +// Later, switch to winter hours +scheduler.setAvailability({ + schedules: [{ days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], start: '09:00', end: '17:00' }], +}) +``` + +## Best Practices + +### 1. Time Format Consistency + +```typescript +// ✅ Good - consistent 24-hour format +{ start: '09:00', end: '17:00' } + +// ❌ Bad - inconsistent formatting +{ start: '9:00', end: '5:00 PM' } // Will fail validation +``` + +### 2. Handling Timezones + +```typescript +// Specify timezone at the scheduler level +const availability = { schedules: [...] } +const scheduler = new AvailabilityScheduler(availability, 'America/Los_Angeles') + +// Or specify timezone when converting availability for a week +const localMondayStart = new Date('2024-01-01T00:00:00Z') // Monday UTC +const busyTimes = weeklyAvailabilityToBusyTimes(availability, localMondayStart, 'America/Los_Angeles') +``` + +### 3. Break Management + +```typescript +// Create breaks with separate time blocks +{ + schedules: [ + { days: ['monday'], start: '09:00', end: '12:00' }, + { days: ['monday'], start: '13:30', end: '17:00' }, + ] +} +``` + +### 4. Validate Early and Often + +```typescript +try { + validateWeeklyAvailability(userProvidedAvailability) + const scheduler = new AvailabilityScheduler(userProvidedAvailability) +} catch (error) { + // Handle validation error +} +``` + +### 5. Week Boundary Awareness + +```typescript +// Always use Monday for week start +function getMondayOfWeek(date: Date): Date { + const day = date.getDay() + const diff = day === 0 ? -6 : 1 - day + const monday = new Date(date) + monday.setDate(date.getDate() + diff) + return new Date(monday.getFullYear(), monday.getMonth(), monday.getDate()) +} + +const mondayStart = getMondayOfWeek(new Date()) +const busyTimes = weeklyAvailabilityToBusyTimes(availability, mondayStart) +``` + +## Common Pitfalls + +- Non-Monday `weekStart` +- Overlapping schedules on the same day +- Using invalid time formats (must be HH:mm or minutes) +- Confusing availability (available periods) with busy times (unavailable periods) + +## Error Handling + +### Validation Errors + +All validation errors include descriptive messages indicating what’s wrong. + +### Runtime Errors + +```typescript +try { + const tuesday = new Date('2024-01-02') // Tuesday + weeklyAvailabilityToBusyTimes(availability, tuesday) +} catch (error) { + // "weekStart must be a Monday (getDay() === 1)" +} +``` diff --git a/docs/core-concepts.md b/docs/core-concepts.md index 1daf3a0..339579d 100644 --- a/docs/core-concepts.md +++ b/docs/core-concepts.md @@ -1,5 +1,7 @@ # Core Concepts +Next: [Availability Scheduler](availability-scheduler.md) + Understanding the fundamental concepts behind the Scheduling SDK will help you make the most of its features and configure it correctly for your use cases. ## Table of Contents @@ -535,4 +537,4 @@ The SDK is optimized for common scheduling scenarios: - Efficient slot generation avoids unnecessary calculations - Early termination when no more slots can fit -Understanding these concepts will help you configure the SDK effectively for your specific scheduling needs. For practical examples, see the [Examples](examples.md) documentation. +Understanding these concepts will help you configure the SDK effectively for your specific scheduling needs. For practical examples, see the [Recipes](recipes.md) documentation. diff --git a/docs/getting-started.md b/docs/getting-started.md index 9476b36..ade3db1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,5 +1,7 @@ # Getting Started +Next: [Core Concepts](core-concepts.md) + ## Installation ```bash @@ -219,6 +221,6 @@ try { - [API Reference](api-reference.md) — Complete method documentation - [Core Concepts](core-concepts.md) — Daily Windows and K-overlaps -- [Availability API](availability-api.md) — Weekly schedules and scheduler-level timezone -- [Examples](examples.md) — Real-world usage scenarios +- [Availability Scheduler](availability-scheduler.md) — Weekly schedules and scheduler-level timezone +- [Recipes](recipes.md) — Real-world usage scenarios - [Performance Guide](performance.md) — Optimization tips diff --git a/docs/examples.md b/docs/recipes.md similarity index 92% rename from docs/examples.md rename to docs/recipes.md index 971b636..2c1e7e4 100644 --- a/docs/examples.md +++ b/docs/recipes.md @@ -1,10 +1,12 @@ -# Usage Examples +# Recipes -This document provides practical examples for common scheduling scenarios using the Scheduling SDK. +Back: [API Reference](api-reference.md) • Next: [Performance](performance.md) + +This document provides practical recipes for common scheduling scenarios using the Scheduling SDK. ## Table of Contents -- [Basic Examples](#basic-examples) +- [Basic Recipes](#basic-recipes) - [Calendar Scheduling](#calendar-scheduling) - [Weekly Availability Scheduling](#weekly-availability-scheduling) - [Resource Management](#resource-management) @@ -12,7 +14,7 @@ This document provides practical examples for common scheduling scenarios using - [Advanced Scenarios](#advanced-scenarios) - [Integration Patterns](#integration-patterns) -## Basic Examples +## Basic Recipes ### Simple Appointment Booking @@ -479,52 +481,6 @@ const earliestSlot = multiLocationService.findEarliestAvailableSlot( console.log(`Earliest available: ${earliestSlot.location} at ${earliestSlot.slot?.start}`) ``` -### Dynamic Availability Updates - -```typescript -class DynamicAvailabilityScheduler { - private scheduler: AvailabilityScheduler - - constructor(initialAvailability: WeeklyAvailability) { - this.scheduler = new AvailabilityScheduler(initialAvailability) - } - - updateHours(newAvailability: WeeklyAvailability) { - // Clear existing busy times and update availability - this.scheduler.clearBusyTimes() - this.scheduler.setAvailability(newAvailability) - } - - addTemporaryBlock(startTime: Date, endTime: Date, reason: string) { - this.scheduler.addBusyTime({ start: startTime, end: endTime }) - console.log(`Added temporary block: ${reason}`) - } - - findAvailabilityForWeek(weekStart: Date) { - const weekEnd = new Date(weekStart.getTime() + 7 * 24 * 60 * 60 * 1000) - - return this.scheduler.findAvailableSlots(weekStart, weekEnd, { - slotDuration: 60, - slotSplit: 60, - padding: 10, - }) - } -} - -// Usage -const dynamicScheduler = new DynamicAvailabilityScheduler({ - schedules: [{ days: ['monday', 'tuesday', 'wednesday'], start: '09:00', end: '17:00' }], -}) - -// Update for holiday hours -dynamicScheduler.updateHours({ - schedules: [{ days: ['monday', 'tuesday', 'wednesday'], start: '10:00', end: '15:00' }], -}) - -// Add temporary blocks -dynamicScheduler.addTemporaryBlock(new Date('2024-01-15T12:00:00Z'), new Date('2024-01-15T13:00:00Z'), 'Team meeting') -``` - ## Meeting Scheduling ### Team Meeting Finder @@ -784,4 +740,4 @@ class PersistentScheduler { } ``` -These examples demonstrate the flexibility and power of the Scheduling SDK for various real-world scenarios. For more information, see the [API Reference](api-reference.md) and [Core Concepts](core-concepts.md) documentation. +These recipes demonstrate the flexibility and power of the Scheduling SDK for various real-world scenarios. For more information, see the [API Reference](api-reference.md) and [Core Concepts](core-concepts.md) documentation. From 3e4f444fa04bc85994042cba5a19eecf37f2ab1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torben=20K=C3=B6hler?= Date: Tue, 9 Sep 2025 16:37:12 +0200 Subject: [PATCH 12/12] create npm pipeline --- .github/workflows/npm-publish.yml | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/npm-publish.yml diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..c9b2a55 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,46 @@ +name: Publish to npm + +on: + release: + types: [published] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test + + - name: Run type checking + run: bun run typecheck + + - name: Run linting + run: bun run lint + + - name: Build package + run: bun run build + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Publish to npm + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file