All files / packages/utils/src/date holidayCalendar.ts

100% Statements 57/57
95.65% Branches 22/23
100% Functions 6/6
100% Lines 50/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 1162x 2x   2x             2x   2x 8x 8x 8x     2x 4x 4x 4x       4x 4x     5x 5x 5x   5x   3x 3x 3x   3x   4x 2x 4x 4x   4x       4x     3x     375x                     378x 378x 378x   378x 378x   378x   375x 375x   375x   270x 4x 4x 4x     268x     4x                     4x 4x 1x     3x   3x              
import { normalizeDateValue, toStartOfDay } from "./comparison";
import { formatDateToYMD } from "./formatting";
 
const WORK_HOURS = [
  // Morning
  { start: 8, startMin: 0, end: 12, endMin: 0 },
  // Afternoon
  { start: 13, startMin: 30, end: 17, endMin: 30 },
];
 
const HOURS_PER_DAY = 8;
 
const parseDate = (date: Date, h: number, m: number): Date => {
  const d = new Date(date);
  d.setHours(h, m, 0, 0);
  return d;
};
 
const overlapHours = (aStart: Date, aEnd: Date, bStart: Date, bEnd: Date): number => {
  const start = Math.max(aStart.getTime(), bStart.getTime());
  const end = Math.min(aEnd.getTime(), bEnd.getTime());
  return Math.max(0, (end - start) / (1000 * 60 * 60));
};
 
function isWeekend(date: Date): boolean {
  const day = date.getDay();
  return day === 0 || day === 6;
}
 
export const calculateLeaveDays = (startISO: string, endISO: string): number => {
  const start = new Date(startISO);
  const end = new Date(endISO);
 
  if (end <= start) return 0;
 
  let totalHours = 0;
  const current = new Date(start);
  current.setHours(0, 0, 0, 0);
 
  while (current <= end) {
    // Skip Saturday & Sunday
    if (!isWeekend(current)) {
      for (const shift of WORK_HOURS) {
        const shiftStart = parseDate(current, shift.start, shift.startMin);
        const shiftEnd = parseDate(current, shift.end, shift.endMin);
 
        totalHours += overlapHours(start, end, shiftStart, shiftEnd);
      }
    }
 
    current.setDate(current.getDate() + 1);
  }
 
  return Number((totalHours / HOURS_PER_DAY).toFixed(2));
};
 
export const isDisabledDate = ({
  date,
  holidays,
  makeUpWorkdays,
  startDate,
}: {
  date: Date;
  holidays: string[];
  makeUpWorkdays: string[];
  startDate?: Date;
}) => {
  const d = new Date(date);
  const day = d.getDay(); // 0 = CN, 6 = T7
  const dateStr = formatDateToYMD(d);
 
  const holidaySet = new Set(holidays);
  const makeUpSet = new Set(makeUpWorkdays);
 
  if (holidaySet.has(dateStr)) return true;
 
  const isWeekendDay = day === 0 || day === 6;
  const isMakeUpDay = makeUpSet.has(dateStr);
 
  if (isWeekendDay && !isMakeUpDay) return true;
 
  if (startDate) {
    const start = new Date(startDate);
    start.setHours(0, 0, 0, 0);
    if (d < start) return true;
  }
 
  return false;
};
 
export const disabledDateByHolidayCalendar = ({
  value,
  holidays,
  makeUpWorkdays,
  disablePastDates = true,
}: {
  value: unknown;
  holidays: string[];
  makeUpWorkdays: string[];
  disablePastDates?: boolean;
}) => {
  const normalizedDate = normalizeDateValue(value);
  if (!normalizedDate) {
    return false;
  }
 
  const minSelectableDate = disablePastDates ? toStartOfDay(new Date()) : undefined;
 
  return isDisabledDate({
    date: normalizedDate,
    holidays,
    makeUpWorkdays,
    startDate: minSelectableDate,
  });
};