feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user