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 diff --git a/README.md b/README.md index c209684..7baa402 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( @@ -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 new file mode 100644 index 0000000..443ed6b --- /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 + +- [Recipes](recipes.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) diff --git a/docs/api-reference.md b/docs/api-reference.md index 6d1e988..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 @@ -42,10 +44,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 +66,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,44 +105,28 @@ 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 -### createScheduler - -Creates a new Scheduler instance. - -```typescript -function createScheduler(busyTimes?: BusyTime[]): Scheduler -``` - -**Parameters:** - -- `busyTimes` (optional): Initial array of busy times. Defaults to empty array. - -**Returns:** +### Creating a Scheduler -- New `Scheduler` instance - -**Example:** +Instantiate the class directly. ```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') }, ]) ``` @@ -256,12 +254,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 +277,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 +308,6 @@ setAvailability(availability: WeeklyAvailability): void ```typescript scheduler.setAvailability({ schedules: [{ days: ['monday', 'tuesday'], start: '09:00', end: '17:00' }], - timezone: 'America/New_York', }) ``` @@ -417,13 +420,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 +491,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 +504,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 deleted file mode 100644 index 0fa5264..0000000 --- a/docs/availability-api.md +++ /dev/null @@ -1,614 +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 - 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 - -The main class for working with weekly availability patterns. - -### Constructor - -```typescript -constructor(availability?: WeeklyAvailability, existingBusyTimes: BusyTime[] = []) -``` - -Creates a new availability scheduler with optional initial availability and existing busy times. - -**Parameters:** - -- `availability` (optional): The weekly availability pattern -- `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) -``` - -### 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): 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) - -**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', - }, - ], - timezone: 'America/New_York', -} - -const scheduler = new AvailabilityScheduler(businessHours) - -// 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 -// βœ… Good - specify timezone when working across timezones -const availability = { - schedules: [...], - timezone: 'America/Los_Angeles' -} - -// βœ… Good - convert dates to appropriate timezone before processing -const localMondayStart = new Date('2024-01-01T08:00:00Z') // UTC Monday 8 AM -``` - -### 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 139c095..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 @@ -84,10 +86,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 +192,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 +299,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 +new Scheduler().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 +new Scheduler().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 @@ -461,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 022d691..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 @@ -26,10 +28,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 +63,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 @@ -97,21 +99,36 @@ 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 ```typescript -import { createScheduler } from 'scheduling-sdk' +import { Scheduler } from 'scheduling-sdk' -const scheduler = createScheduler() +const scheduler = new Scheduler() // Add existing appointments scheduler.addBusyTimes([ @@ -145,6 +162,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 @@ -160,8 +188,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]) @@ -191,7 +219,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 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/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 diff --git a/docs/examples.md b/docs/recipes.md similarity index 89% rename from docs/examples.md rename to docs/recipes.md index b5a27c5..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,15 +14,15 @@ 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 ```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 +42,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 +59,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 +78,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 +116,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 +156,7 @@ class MeetingRoomScheduler { constructor(roomNames: string[]) { roomNames.forEach(name => { - this.rooms.set(name, createScheduler()) + this.rooms.set(name, new Scheduler()) }) } @@ -227,7 +229,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, }) @@ -320,10 +322,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( @@ -480,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 @@ -535,7 +490,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) { @@ -610,8 +565,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]) @@ -647,7 +602,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 = [] @@ -709,7 +664,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 @@ -744,7 +699,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) { @@ -785,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. diff --git a/package.json b/package.json index a172696..d9532e2 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", @@ -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", 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) -} diff --git a/tests/index.test.ts b/tests/index.test.ts index 737635e..6f3691c 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, @@ -41,31 +39,9 @@ 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('createScheduler convenience function', () => { + describe('Scheduler constructor behavior', () => { it('should create a Scheduler instance with no arguments', () => { - const scheduler = createScheduler() - 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,52 +52,14 @@ 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) }) - 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 = createScheduler(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 = createScheduler() - const scheduler2 = createScheduler() - - expect(scheduler1).not.toBe(scheduler2) - expect(scheduler1).toBeInstanceOf(Scheduler) - expect(scheduler2).toBeInstanceOf(Scheduler) - }) - 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 +75,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') @@ -183,38 +121,19 @@ 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(createScheduler).toBeDefined() - expect(typeof createScheduler).toBe('function') - }) - - it('should maintain consistent API across export methods', () => { - const directScheduler = new Scheduler() - const convenienceScheduler = createScheduler() - - // Both should have the same methods - expect(typeof directScheduler.findAvailableSlots).toBe('function') - expect(typeof convenienceScheduler.findAvailableSlots).toBe('function') - - expect(typeof directScheduler.addBusyTimes).toBe('function') - expect(typeof convenienceScheduler.addBusyTimes).toBe('function') - - expect(typeof directScheduler.clearBusyTimes).toBe('function') - expect(typeof convenienceScheduler.clearBusyTimes).toBe('function') - - expect(typeof directScheduler.getBusyTimes).toBe('function') - expect(typeof convenienceScheduler.getBusyTimes).toBe('function') + expect(typeof Scheduler).toBe('function') + expect(AvailabilityScheduler).toBeDefined() }) }) - describe('createAvailabilityScheduler convenience function', () => { + describe('AvailabilityScheduler constructor usage', () => { 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 +142,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 +156,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'), 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') }) })