/** * Bridge Days Calculator * Calculates optimal vacation days based on public holidays */ /** * Calculate bridge days and optimal vacation periods for a month * @param {number} year - The year to calculate for * @param {number} month - The month (0-11) * @param {string} bundesland - The German state code * @returns {Array} Array of bridge day recommendations */ function calculateBridgeDays(year, month, bundesland) { const recommendations = []; // Get all holidays for the year const holidays = getPublicHolidays(year, bundesland); // Create a calendar map for the entire year const calendar = createYearCalendar(year, holidays); // Find all work day blocks (consecutive work days between weekends/holidays) const workBlocks = findWorkDayBlocks(calendar); // Evaluate each block and calculate benefit workBlocks.forEach(block => { const benefit = evaluateBlock(block, calendar); if (benefit.ratio >= 2.0) { // Only show if at least 2x benefit recommendations.push(benefit); } }); // Sort by benefit ratio (best deals first) recommendations.sort((a, b) => b.ratio - a.ratio); // Filter for the specific month const monthRecommendations = recommendations.filter(rec => { const startDate = new Date(rec.startDate); return startDate.getMonth() === month && startDate.getFullYear() === year; }); return monthRecommendations; } /** * Create a calendar map for the entire year * @param {number} year - The year * @param {Array} holidays - Array of holiday objects * @returns {Map} Map of date strings to day types */ function createYearCalendar(year, holidays) { const calendar = new Map(); // Create holiday map for fast lookup const holidayMap = new Map(); holidays.forEach(h => { const dateStr = formatDateKey(h.date); holidayMap.set(dateStr, h.name); }); // Process each day of the year for (let month = 0; month < 12; month++) { const daysInMonth = new Date(year, month + 1, 0).getDate(); for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dateStr = formatDateKey(date); const dayOfWeek = date.getDay(); let type; if (holidayMap.has(dateStr)) { type = { status: 'HOLIDAY', name: holidayMap.get(dateStr) }; } else if (dayOfWeek === 0 || dayOfWeek === 6) { type = { status: 'WEEKEND' }; } else { type = { status: 'WORKDAY' }; } calendar.set(dateStr, type); } } return calendar; } /** * Format date as YYYY-MM-DD */ function formatDateKey(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Find all work day blocks in the calendar * @param {Map} calendar - The calendar map * @returns {Array} Array of work day blocks */ function findWorkDayBlocks(calendar) { const blocks = []; let currentBlock = null; // Sort dates for sequential processing const sortedDates = Array.from(calendar.keys()).sort(); sortedDates.forEach(dateStr => { const dayType = calendar.get(dateStr); if (dayType.status === 'WORKDAY') { if (!currentBlock) { currentBlock = { startDate: dateStr, endDate: dateStr, days: [dateStr] }; } else { currentBlock.endDate = dateStr; currentBlock.days.push(dateStr); } } else { if (currentBlock) { blocks.push(currentBlock); currentBlock = null; } } }); // Don't forget the last block if (currentBlock) { blocks.push(currentBlock); } return blocks; } /** * Evaluate a work day block and calculate benefit * @param {Object} block - The work day block * @param {Map} calendar - The calendar map * @returns {Object} Benefit information */ function evaluateBlock(block, calendar) { const vacationDaysNeeded = block.days.length; // Find the extended free period (including surrounding weekends/holidays) let startDate = new Date(block.startDate); let endDate = new Date(block.endDate); // Extend backwards to include preceding weekends/holidays let currentDate = new Date(startDate); currentDate.setDate(currentDate.getDate() - 1); while (true) { const dateStr = formatDateKey(currentDate); const dayType = calendar.get(dateStr); if (!dayType || dayType.status === 'WORKDAY') break; startDate = new Date(currentDate); currentDate.setDate(currentDate.getDate() - 1); } // Extend forwards to include following weekends/holidays currentDate = new Date(endDate); currentDate.setDate(currentDate.getDate() + 1); while (true) { const dateStr = formatDateKey(currentDate); const dayType = calendar.get(dateStr); if (!dayType || dayType.status === 'WORKDAY') break; endDate = new Date(currentDate); currentDate.setDate(currentDate.getDate() + 1); } // Calculate total free days const totalFreeDays = Math.floor((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1; // Calculate benefit ratio const ratio = totalFreeDays / vacationDaysNeeded; // Find holidays in the period for description const holidaysInPeriod = []; currentDate = new Date(startDate); while (currentDate <= endDate) { const dateStr = formatDateKey(currentDate); const dayType = calendar.get(dateStr); if (dayType && dayType.status === 'HOLIDAY') { holidaysInPeriod.push(dayType.name); } currentDate.setDate(currentDate.getDate() + 1); } return { startDate: formatDateKey(startDate), endDate: formatDateKey(endDate), vacationDays: block.days, vacationDaysNeeded: vacationDaysNeeded, totalFreeDays: totalFreeDays, ratio: ratio, holidays: holidaysInPeriod }; } /** * Get a human-readable description for a bridge day recommendation * @param {Object} recommendation - The recommendation object * @returns {string} Description text */ function getBridgeDayDescription(recommendation) { const { vacationDaysNeeded, totalFreeDays, ratio, holidays } = recommendation; let description = `${vacationDaysNeeded} Urlaubstag${vacationDaysNeeded > 1 ? 'e' : ''} für ${totalFreeDays} freie Tage`; if (holidays.length > 0) { description += ` (inkl. ${holidays.join(', ')})`; } description += ` - ${ratio.toFixed(1)}x Ertrag`; return description; }