Improve algorithms
This commit is contained in:
@@ -1,6 +1,34 @@
|
|||||||
import Holidays from 'date-holidays';
|
import Holidays from 'date-holidays';
|
||||||
|
|
||||||
// getHolidaysForYear(countryCode: string, year: number): { date: Date, name: string }[]
|
// Constants
|
||||||
|
const MS_IN_A_DAY = 86400000;
|
||||||
|
const MAX_GAP_LENGTH = 5;
|
||||||
|
|
||||||
|
// Helper function to check if a date is a weekend
|
||||||
|
const isWeekend = date => date.getDay() === 0 || date.getDay() === 6;
|
||||||
|
|
||||||
|
// Helper function to check if two dates are the same day
|
||||||
|
const isSameDay = (date1, date2) =>
|
||||||
|
date1.getFullYear() === date2.getFullYear() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getDate() === date2.getDate();
|
||||||
|
|
||||||
|
// Helper function to generate a unique key for a date
|
||||||
|
const dateKey = date => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||||
|
|
||||||
|
// Helper function to check if a date is a holiday
|
||||||
|
const isHoliday = (date, holidays) => holidays.some(h => isSameDay(h.date, date));
|
||||||
|
|
||||||
|
// Helper function to check if a date is a day off
|
||||||
|
const isDayOff = (date, allDaysOffSet) => allDaysOffSet.has(dateKey(date));
|
||||||
|
|
||||||
|
// Helper function to calculate the number of days between two dates
|
||||||
|
const daysBetween = (startDate, endDate) => Math.round((endDate - startDate) / MS_IN_A_DAY);
|
||||||
|
|
||||||
|
// Helper function to format a date
|
||||||
|
const formatDate = date => date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
|
||||||
|
// Get holidays for a specific year and country
|
||||||
export function getHolidaysForYear(countryCode, year) {
|
export function getHolidaysForYear(countryCode, year) {
|
||||||
const hd = new Holidays(countryCode);
|
const hd = new Holidays(countryCode);
|
||||||
return hd.getHolidays(year)
|
return hd.getHolidays(year)
|
||||||
@@ -11,66 +39,171 @@ export function getHolidaysForYear(countryCode, year) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// optimizeDaysOff(holidays: { date: Date, name: string }[], year: number, daysOff: number): Date[]
|
// Optimize days off to create the longest possible chains
|
||||||
export function optimizeDaysOff(holidays, year, daysOff) {
|
export function optimizeDaysOff(holidays, year, daysOff) {
|
||||||
const weekends = getWeekends(year);
|
const weekends = getWeekends(year);
|
||||||
const allDaysOff = [...holidays.map(h => h.date), ...weekends];
|
const allDaysOffSet = new Set([...holidays.map(h => dateKey(h.date)), ...weekends.map(d => dateKey(d))]);
|
||||||
allDaysOff.sort((a, b) => a - b);
|
|
||||||
|
|
||||||
const gaps = findGaps(allDaysOff, year);
|
let rankedGaps = rankGapsByEfficiency(findGaps(allDaysOffSet, year), allDaysOffSet);
|
||||||
const rankedGaps = rankGapsByEfficiency(gaps);
|
|
||||||
|
|
||||||
return selectDaysOff(rankedGaps, daysOff, allDaysOff);
|
return selectDaysOff(rankedGaps, daysOff, allDaysOffSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateConsecutiveDaysOff(holidays: { date: Date, name: string }[], optimizedDaysOff: Date[], year: number): { startDate: string, endDate: string, includesHoliday: boolean, message: string }[]
|
// Calculate consecutive days off
|
||||||
export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) {
|
export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) {
|
||||||
let consecutiveDaysOff = [];
|
|
||||||
const allDays = [...holidays.map(h => h.date), ...optimizedDaysOff];
|
const allDays = [...holidays.map(h => h.date), ...optimizedDaysOff];
|
||||||
allDays.sort((a, b) => a - b);
|
allDays.sort((a, b) => a - b);
|
||||||
|
|
||||||
let currentGroup = [];
|
return findConsecutiveDaysOff(allDays, holidays, optimizedDaysOff);
|
||||||
let includesHoliday = false;
|
}
|
||||||
|
|
||||||
|
// Get all weekends for a specific year
|
||||||
|
function getWeekends(year) {
|
||||||
|
const weekends = [];
|
||||||
|
for (let month = 0; month < 12; month++) {
|
||||||
|
for (let day = 1; day <= 31; day++) {
|
||||||
|
const date = new Date(year, month, day);
|
||||||
|
if (date.getMonth() !== month) break;
|
||||||
|
if (isWeekend(date)) weekends.push(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return weekends;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find gaps between days off
|
||||||
|
function findGaps(allDaysOffSet, year) {
|
||||||
|
const gaps = [];
|
||||||
|
let currentGapStart = null;
|
||||||
|
|
||||||
for (let month = 0; month < 12; month++) {
|
for (let month = 0; month < 12; month++) {
|
||||||
for (let day = 1; day <= 31; day++) {
|
for (let day = 1; day <= 31; day++) {
|
||||||
const date = new Date(year, month, day);
|
const date = new Date(year, month, day);
|
||||||
if (date.getMonth() !== month) break;
|
if (date.getMonth() !== month) break;
|
||||||
|
|
||||||
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
|
const isDayOff = allDaysOffSet.has(dateKey(date));
|
||||||
const isHoliday = holidays.some(h => h.date.getTime() === date.getTime());
|
|
||||||
const isDayOff = optimizedDaysOff.some(d => d.getTime() === date.getTime());
|
|
||||||
|
|
||||||
if (isWeekend || isHoliday || isDayOff) {
|
if (!isDayOff && !isWeekend(date)) {
|
||||||
currentGroup.push(date);
|
if (!currentGapStart) currentGapStart = date;
|
||||||
if (isHoliday) includesHoliday = true;
|
} else if (currentGapStart) {
|
||||||
} else if (currentGroup.length > 0) {
|
const gapLength = daysBetween(currentGapStart, date);
|
||||||
if (currentGroup.some(d => optimizedDaysOff.some(od => od.getTime() === d.getTime()))) {
|
if (gapLength > 0 && gapLength <= MAX_GAP_LENGTH) {
|
||||||
const startDate = currentGroup[0];
|
gaps.push({ start: currentGapStart, end: new Date(date.getTime() - MS_IN_A_DAY), gapLength });
|
||||||
const endDate = currentGroup[currentGroup.length - 1];
|
|
||||||
const totalDays = Math.round((endDate - startDate) / (1000 * 60 * 60 * 24) + 1);
|
|
||||||
const usedDaysOff = currentGroup.filter(d => optimizedDaysOff.some(od => od.getTime() === d.getTime())).length;
|
|
||||||
const message = `${usedDaysOff} days off -> ${totalDays} days`;
|
|
||||||
|
|
||||||
consecutiveDaysOff.push({
|
|
||||||
startDate: formatDate(startDate),
|
|
||||||
endDate: formatDate(endDate),
|
|
||||||
includesHoliday,
|
|
||||||
message
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
currentGroup = [];
|
currentGapStart = null;
|
||||||
includesHoliday = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentGroup.length > 0 && currentGroup.some(d => optimizedDaysOff.some(od => od.getTime() === d.getTime()))) {
|
if (currentGapStart) {
|
||||||
|
const lastDayOfYear = new Date(year, 11, 31);
|
||||||
|
const gapLength = daysBetween(currentGapStart, lastDayOfYear) + 1;
|
||||||
|
if (gapLength > 0 && gapLength <= MAX_GAP_LENGTH) {
|
||||||
|
gaps.push({ start: currentGapStart, end: lastDayOfYear, gapLength });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rank gaps by efficiency
|
||||||
|
function rankGapsByEfficiency(gaps, allDaysOffSet) {
|
||||||
|
return gaps.map(gap => {
|
||||||
|
const backward = calculateChain(gap.start, gap.gapLength, allDaysOffSet, 'backward');
|
||||||
|
const forward = calculateChain(gap.end, gap.gapLength, allDaysOffSet, 'forward');
|
||||||
|
|
||||||
|
return forward.chainLength > backward.chainLength || (forward.chainLength === backward.chainLength && forward.usedDaysOff <= backward.usedDaysOff)
|
||||||
|
? { ...gap, ...forward, fillFrom: 'end' }
|
||||||
|
: { ...gap, ...backward, fillFrom: 'start' };
|
||||||
|
}).sort((a, b) => a.gapLength - b.gapLength || b.chainLength - a.chainLength || a.usedDaysOff - b.usedDaysOff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate potential chain length and days off used
|
||||||
|
function calculateChain(startDate, gapLength, allDaysOffSet, direction) {
|
||||||
|
let chainLength = gapLength;
|
||||||
|
let usedDaysOff = 0;
|
||||||
|
let currentDate = new Date(startDate);
|
||||||
|
|
||||||
|
const increment = direction === 'backward' ? -1 : 1;
|
||||||
|
const boundaryCheck = direction === 'backward' ? -MS_IN_A_DAY : MS_IN_A_DAY;
|
||||||
|
|
||||||
|
while (allDaysOffSet.has(dateKey(new Date(currentDate.getTime() + boundaryCheck))) || isWeekend(new Date(currentDate.getTime() + boundaryCheck))) {
|
||||||
|
chainLength++;
|
||||||
|
currentDate.setDate(currentDate.getDate() + increment);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < gapLength; i++) {
|
||||||
|
const potentialDayOff = new Date(startDate);
|
||||||
|
potentialDayOff.setDate(potentialDayOff.getDate() + (i * increment));
|
||||||
|
if (!allDaysOffSet.has(dateKey(potentialDayOff)) && !isWeekend(potentialDayOff)) {
|
||||||
|
usedDaysOff++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { chainLength, usedDaysOff };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select days off based on ranked gaps
|
||||||
|
function selectDaysOff(rankedGaps, daysOff, allDaysOffSet) {
|
||||||
|
const selectedDays = [];
|
||||||
|
|
||||||
|
while (daysOff > 0 && rankedGaps.length > 0) {
|
||||||
|
const gap = rankedGaps.shift(); // Get the highest-ranked gap
|
||||||
|
|
||||||
|
// Determine the direction and starting point for filling the gap
|
||||||
|
const increment = gap.fillFrom === 'start' ? 1 : -1;
|
||||||
|
const startDate = gap.fillFrom === 'start' ? gap.start : gap.end;
|
||||||
|
|
||||||
|
for (let i = 0; i < gap.gapLength && daysOff > 0; i++) {
|
||||||
|
const potentialDayOff = new Date(startDate);
|
||||||
|
potentialDayOff.setDate(potentialDayOff.getDate() + (i * increment));
|
||||||
|
|
||||||
|
if (!allDaysOffSet.has(dateKey(potentialDayOff)) && !isWeekend(potentialDayOff)) {
|
||||||
|
selectedDays.push(potentialDayOff);
|
||||||
|
allDaysOffSet.add(dateKey(potentialDayOff));
|
||||||
|
daysOff--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate gaps and re-rank them after each assignment
|
||||||
|
const newGaps = findGaps(allDaysOffSet, new Date().getFullYear());
|
||||||
|
rankedGaps = rankGapsByEfficiency(newGaps, allDaysOffSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find consecutive days off
|
||||||
|
function findConsecutiveDaysOff(allDays, holidays, optimizedDaysOff) {
|
||||||
|
let consecutiveDaysOff = [];
|
||||||
|
let currentGroup = [];
|
||||||
|
let includesHoliday = false;
|
||||||
|
|
||||||
|
allDays.forEach(date => {
|
||||||
|
if (isWeekend(date) || isHoliday(date, holidays) || isDayOff(date, new Set(optimizedDaysOff.map(d => dateKey(d))))) {
|
||||||
|
currentGroup.push(date);
|
||||||
|
if (isHoliday(date, holidays)) includesHoliday = true;
|
||||||
|
} else if (currentGroup.length > 0) {
|
||||||
|
addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff, includesHoliday);
|
||||||
|
currentGroup = [];
|
||||||
|
includesHoliday = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentGroup.length > 0) {
|
||||||
|
addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff, includesHoliday);
|
||||||
|
}
|
||||||
|
|
||||||
|
return consecutiveDaysOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add consecutive days off to the list
|
||||||
|
function addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff, includesHoliday) {
|
||||||
|
if (currentGroup.some(d => isDayOff(d, new Set(optimizedDaysOff.map(d => dateKey(d)))))) {
|
||||||
const startDate = currentGroup[0];
|
const startDate = currentGroup[0];
|
||||||
const endDate = currentGroup[currentGroup.length - 1];
|
const endDate = currentGroup[currentGroup.length - 1];
|
||||||
const totalDays = Math.round((endDate - startDate) / (1000 * 60 * 60 * 24) + 1);
|
const totalDays = daysBetween(startDate, endDate) + 1;
|
||||||
const usedDaysOff = currentGroup.filter(d => optimizedDaysOff.some(od => od.getTime() === d.getTime())).length;
|
const usedDaysOff = currentGroup.filter(d => isDayOff(d, new Set(optimizedDaysOff.map(d => dateKey(d))))).length;
|
||||||
const message = `${usedDaysOff} day off turns into ${totalDays} days off`;
|
const message = `${usedDaysOff} days off -> ${totalDays} days`;
|
||||||
|
|
||||||
consecutiveDaysOff.push({
|
consecutiveDaysOff.push({
|
||||||
startDate: formatDate(startDate),
|
startDate: formatDate(startDate),
|
||||||
@@ -79,65 +212,4 @@ export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) {
|
|||||||
message
|
message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return consecutiveDaysOff;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWeekends(year) {
|
|
||||||
const weekends = [];
|
|
||||||
for (let month = 0; month < 12; month++) {
|
|
||||||
for (let day = 1; day <= 31; day++) {
|
|
||||||
const date = new Date(year, month, day);
|
|
||||||
if (date.getMonth() !== month) break;
|
|
||||||
if (date.getDay() === 0 || date.getDay() === 6) {
|
|
||||||
weekends.push(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return weekends;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findGaps(allDaysOff, year) {
|
|
||||||
const gaps = [];
|
|
||||||
for (let i = 0; i < allDaysOff.length - 1; i++) {
|
|
||||||
const start = allDaysOff[i];
|
|
||||||
const end = allDaysOff[i + 1];
|
|
||||||
const gapLength = (end - start) / (1000 * 60 * 60 * 24) - 1;
|
|
||||||
if (gapLength > 0 && gapLength <= 4) {
|
|
||||||
gaps.push({ start, end, gapLength });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gaps;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rankGapsByEfficiency(gaps) {
|
|
||||||
return gaps.map(gap => {
|
|
||||||
const potentialChainLength = gap.gapLength + 2; // including weekends/holidays
|
|
||||||
const efficiency = potentialChainLength / gap.gapLength;
|
|
||||||
return { ...gap, potentialChainLength, efficiency };
|
|
||||||
}).sort((a, b) => b.efficiency - a.efficiency);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectDaysOff(rankedGaps, daysOff, allDaysOff) {
|
|
||||||
const selectedDays = [];
|
|
||||||
const allDaysOffSet = new Set(allDaysOff.map(date => date.getTime()));
|
|
||||||
|
|
||||||
for (const gap of rankedGaps) {
|
|
||||||
for (let i = 1; i <= gap.gapLength && daysOff > 0; i++) {
|
|
||||||
const potentialDayOff = new Date(gap.start);
|
|
||||||
potentialDayOff.setDate(potentialDayOff.getDate() + i);
|
|
||||||
|
|
||||||
// Ensure the day is not a weekend or holiday
|
|
||||||
if (!allDaysOffSet.has(potentialDayOff.getTime()) && potentialDayOff.getDay() !== 0 && potentialDayOff.getDay() !== 6) {
|
|
||||||
selectedDays.push(potentialDayOff);
|
|
||||||
daysOff--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (daysOff <= 0) break;
|
|
||||||
}
|
|
||||||
return selectedDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(date) {
|
|
||||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user