Files
CapaKraken/packages/api/src/lib/vacation-day-count.ts
T

113 lines
2.8 KiB
TypeScript

import { getCalendarHolidayStrings, toIsoDate } from "./holiday-availability.js";
type VacationSpan = {
startDate: Date;
endDate: Date;
isHalfDay: boolean;
};
type HolidayContext = {
countryCode?: string | null | undefined;
federalState?: string | null | undefined;
metroCityName?: string | null | undefined;
calendarHolidayStrings?: string[] | undefined;
publicHolidayStrings?: string[] | undefined;
};
type CountVacationChargeableDaysInput = HolidayContext & {
vacation: VacationSpan;
periodStart?: Date | undefined;
periodEnd?: Date | undefined;
};
function clampToDay(value: Date): Date {
const date = new Date(value);
date.setUTCHours(0, 0, 0, 0);
return date;
}
function getOverlapRange(
startDate: Date,
endDate: Date,
periodStart?: Date,
periodEnd?: Date,
): { start: Date; end: Date } | null {
const startBoundary = clampToDay(periodStart ?? startDate);
const endBoundary = clampToDay(periodEnd ?? endDate);
const overlapStart = clampToDay(new Date(Math.max(startDate.getTime(), startBoundary.getTime())));
const overlapEnd = clampToDay(new Date(Math.min(endDate.getTime(), endBoundary.getTime())));
if (overlapStart > overlapEnd) {
return null;
}
return { start: overlapStart, end: overlapEnd };
}
export function countCalendarDaysInPeriod(
vacation: VacationSpan,
periodStart?: Date,
periodEnd?: Date,
): number {
const overlap = getOverlapRange(vacation.startDate, vacation.endDate, periodStart, periodEnd);
if (!overlap) {
return 0;
}
if (vacation.isHalfDay) {
return 0.5;
}
const ms = overlap.end.getTime() - overlap.start.getTime();
return Math.round(ms / 86_400_000) + 1;
}
export function countVacationChargeableDays(
input: CountVacationChargeableDaysInput,
): number {
const overlap = getOverlapRange(
input.vacation.startDate,
input.vacation.endDate,
input.periodStart,
input.periodEnd,
);
if (!overlap) {
return 0;
}
const holidaySet = new Set(
input.calendarHolidayStrings
? input.calendarHolidayStrings.filter((isoDate) => isoDate >= toIsoDate(overlap.start) && isoDate <= toIsoDate(overlap.end))
: getCalendarHolidayStrings(
overlap.start,
overlap.end,
input.countryCode,
input.federalState,
input.metroCityName,
),
);
for (const isoDate of input.publicHolidayStrings ?? []) {
if (isoDate >= toIsoDate(overlap.start) && isoDate <= toIsoDate(overlap.end)) {
holidaySet.add(isoDate);
}
}
if (input.vacation.isHalfDay) {
return holidaySet.has(toIsoDate(overlap.start)) ? 0 : 0.5;
}
let total = 0;
const cursor = new Date(overlap.start);
while (cursor <= overlap.end) {
if (!holidaySet.has(toIsoDate(cursor))) {
total += 1;
}
cursor.setUTCDate(cursor.getUTCDate() + 1);
}
return total;
}