$suspensions */ public function countSuspendedDaysInMonth( DateTimeImmutable $monthStart, DateTimeImmutable $monthEnd, array $suspensions ): int { $total = 0; foreach ($suspensions as $suspension) { $sStart = $suspension->getStartDate(); $sEnd = $suspension->getEndDate() ?? $monthEnd; $overlapStart = $sStart > $monthStart ? $sStart : $monthStart; $overlapEnd = $sEnd < $monthEnd ? $sEnd : $monthEnd; if ($overlapStart > $overlapEnd) { continue; } $total += ((int) $overlapEnd->diff($overlapStart)->format('%a')) + 1; } return $total; } /** * Return adjusted suspensions where the first month of each suspension is excluded (grace period). * * @param list $suspensions * * @return list */ public function applyFirstMonthGrace(array $suspensions): array { $adjusted = []; foreach ($suspensions as $suspension) { $gracedStart = $suspension->getStartDate()->modify('+1 month'); $end = $suspension->getEndDate(); if ($end instanceof DateTimeImmutable && $gracedStart > $end) { continue; } $copy = new ContractSuspension(); $copy->setStartDate($gracedStart); $copy->setEndDate($end); $adjusted[] = $copy; } return $adjusted; } /** * Count business days (Mon-Fri, excl. public holidays) suspended within a period. * * @param list $suspensions * @param array $publicHolidays map of Y-m-d => label */ public function countSuspendedBusinessDays( DateTimeImmutable $periodStart, DateTimeImmutable $periodEnd, array $suspensions, array $publicHolidays ): int { $total = 0; foreach ($suspensions as $suspension) { $sStart = $suspension->getStartDate(); $sEnd = $suspension->getEndDate() ?? $periodEnd; $overlapStart = $sStart > $periodStart ? $sStart : $periodStart; $overlapEnd = $sEnd < $periodEnd ? $sEnd : $periodEnd; if ($overlapStart > $overlapEnd) { continue; } for ($cursor = $overlapStart; $cursor <= $overlapEnd; $cursor = $cursor->modify('+1 day')) { $weekDay = (int) $cursor->format('N'); $dayKey = $cursor->format('Y-m-d'); if ($weekDay <= 5 && !isset($publicHolidays[$dayKey])) { ++$total; } } } return $total; } }