Skip to main content
Glama
pshempel

MCP Time Server Node

by pshempel
businessHoursHelpers.ts8.73 kB
/** * Helper functions for business hours calculations. * Extracted from calculateBusinessHours to reduce complexity. * Each function has a single, clear responsibility. */ import { format, differenceInMinutes, eachDayOfInterval } from 'date-fns'; import { formatInTimeZone } from 'date-fns-tz'; import { ValidationError } from '../adapters/mcp-sdk/errors'; import type { BusinessHours, WeeklyBusinessHours, DayBusinessHours } from '../types'; import { debug } from './debug'; // Day names for output const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; /** * Check if hours object is a weekly schedule */ function isWeeklyBusinessHours( hours: BusinessHours | WeeklyBusinessHours ): hours is WeeklyBusinessHours { const isWeekly = typeof hours === 'object' && !('start' in hours); debug.business('Schedule type detected:', { type: isWeekly ? 'weekly' : 'daily', hasStartProperty: 'start' in hours, }); return isWeekly; } /** * Validate a single BusinessHours object * @throws Error if hours are invalid */ function validateSingleBusinessHours(hours: BusinessHours): void { if (hours.start.hour < 0 || hours.start.hour > 23) { debug.error('Invalid start hour: %d', hours.start.hour); throw new Error('Invalid start hour'); } if (hours.start.minute < 0 || hours.start.minute > 59) { debug.error('Invalid start minute: %d', hours.start.minute); throw new Error('Invalid start minute'); } if (hours.end.hour < 0 || hours.end.hour > 23) { debug.error('Invalid end hour: %d', hours.end.hour); throw new Error('Invalid end hour'); } if (hours.end.minute < 0 || hours.end.minute > 59) { debug.error('Invalid end minute: %d', hours.end.minute); throw new Error('Invalid end minute'); } } /** * Validate business hours structure (single or weekly) * Single responsibility: Validate business hours configuration * @param hours - Business hours to validate * @throws Error with details if validation fails */ export function validateBusinessHoursStructure( hours: BusinessHours | WeeklyBusinessHours | undefined ): void { if (!hours) return; if (isWeeklyBusinessHours(hours)) { // Validate weekly schedule for (const [day, dayHours] of Object.entries(hours)) { const dayNum = parseInt(day, 10); if (dayNum < 0 || dayNum > 6) { debug.error('Invalid day of week: %s', day); throw new ValidationError(`Invalid day of week: ${day}`, { day, field: 'business_hours' }); } if (dayHours !== null) { try { validateSingleBusinessHours(dayHours as BusinessHours); } catch (error) { debug.error( 'Invalid business hours for day %s: %s', day, error instanceof Error ? error.message : String(error) ); throw new ValidationError( `Invalid business hours for day ${day}: ${error instanceof Error ? error.message : String(error)}`, { day, hours: dayHours as BusinessHours, field: 'business_hours' } ); } } } } else { // Validate single business hours try { validateSingleBusinessHours(hours); } catch (error) { debug.error( 'Invalid business hours: %s', error instanceof Error ? error.message : String(error) ); throw new ValidationError( `Invalid business hours: ${error instanceof Error ? error.message : String(error)}`, { business_hours: hours, field: 'business_hours' } ); } } } /** * Get all dates between start and end in the business timezone * Single responsibility: Generate date strings in business timezone * @returns Array of date strings in YYYY-MM-DD format */ export function getDatesInBusinessTimezone( startDate: Date, endDate: Date, timezone: string ): string[] { const dates = new Set<string>(); // Get all dates in the range const allDates = eachDayOfInterval({ start: startDate, end: endDate, }); // Convert each to business timezone date string for (const date of allDates) { dates.add(formatInTimeZone(date, timezone, 'yyyy-MM-dd')); } // Also ensure we have the end date in case of timezone edge cases dates.add(formatInTimeZone(endDate, timezone, 'yyyy-MM-dd')); const result = Array.from(dates).sort(); // Log if timezone caused date count to differ from simple day count if (result.length !== allDates.length) { debug.decision('Timezone affected date range', { timezone, simpleDayCount: allDates.length, businessDayCount: result.length, dates: result, }); } return result; } /** * Get day information for a date string in a timezone * Single responsibility: Extract day-of-week information * @returns Object with dayOfWeek (0-6) and dayName */ export function getDayInfo( dateStr: string, timezone: string ): { dayOfWeek: number; dayName: string } { // Create a date at noon to avoid DST issues const date = new Date(`${dateStr}T12:00:00`); // Get day of week in business timezone // 'c' format gives us 1=Sunday, 2=Monday, etc., so subtract 1 const dayOfWeek = parseInt(formatInTimeZone(date, timezone, 'c'), 10) - 1; const dayName = DAY_NAMES[dayOfWeek]; return { dayOfWeek, dayName }; } /** * Check if a day is a work day * Single responsibility: Determine if work happens on this day * @param dateStr - Date string in YYYY-MM-DD format * @param dayOfWeek - Day of week (0=Sunday, 6=Saturday) * @param isWeekend - Whether this day is a weekend * @param holidays - Array of holiday dates * @param includeWeekends - Whether to include weekends as work days * @returns true if this is a work day */ export function isWorkDay( dateStr: string, dayOfWeek: number, isWeekend: boolean, holidays: Date[], includeWeekends: boolean, timezone?: string ): boolean { // Check if it's a holiday // Use formatInTimeZone if timezone provided, otherwise use format for backward compatibility const isHoliday = timezone ? holidays.some((h) => formatInTimeZone(h, timezone, 'yyyy-MM-dd') === dateStr) : holidays.some((h) => format(h, 'yyyy-MM-dd') === dateStr); if (isHoliday) { debug.business('Day is a holiday:', { date: dateStr }); return false; } // Check weekend if (isWeekend && !includeWeekends) { debug.business('Weekend excluded:', { date: dateStr, dayOfWeek, dayName: DAY_NAMES[dayOfWeek], }); return false; } return true; } /** * Calculate business minutes for a single day * Single responsibility: Calculate overlap between business hours and time range * @returns Number of business minutes on this day */ export function calculateDayBusinessMinutes( dateStr: string, businessHours: BusinessHours | null, dayBusinessStart: Date, dayBusinessEnd: Date, rangeStart: Date, rangeEnd: Date ): number { // No business hours means closed if (!businessHours) { debug.business('Day is closed:', { date: dateStr }); return 0; } // Find the overlap between business hours and the range let effectiveStart = dayBusinessStart; let effectiveEnd = dayBusinessEnd; // Adjust for range start if (rangeStart > effectiveEnd) { // Range starts after business hours end debug.business('Range starts after business hours:', { date: dateStr, rangeStart: format(rangeStart, 'HH:mm'), businessEnd: format(effectiveEnd, 'HH:mm'), }); return 0; } if (rangeStart > effectiveStart) { effectiveStart = rangeStart; } // Adjust for range end if (rangeEnd < effectiveStart) { // Range ends before business hours start debug.business('Range ends before business hours:', { date: dateStr, rangeEnd: format(rangeEnd, 'HH:mm'), businessStart: format(effectiveStart, 'HH:mm'), }); return 0; } if (rangeEnd < effectiveEnd) { effectiveEnd = rangeEnd; } // Calculate minutes const minutes = Math.max(0, differenceInMinutes(effectiveEnd, effectiveStart)); if (minutes > 0) { debug.business('Calculated business minutes:', { date: dateStr, minutes, effectiveStart: format(effectiveStart, 'HH:mm'), effectiveEnd: format(effectiveEnd, 'HH:mm'), }); } return minutes; } /** * Build a day result object * Single responsibility: Create the DayBusinessHours structure */ export function buildDayResult( date: string, dayOfWeek: string, businessMinutes: number, isWeekend: boolean, isHoliday: boolean ): DayBusinessHours { return { date, day_of_week: dayOfWeek, business_minutes: businessMinutes, is_weekend: isWeekend, is_holiday: isHoliday, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pshempel/mcp-time-server-node'

If you have feedback or need assistance with the MCP directory API, please join our Discord server