220 lines
6.4 KiB
JavaScript
220 lines
6.4 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|