attendanceModel = $attendanceModel; $this->configModel = $configModel; $this->calendarModel = $calendarModel; } public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array { $absences = $this->attendanceModel->getTotalAbsences($studentId, $semester, $schoolYear); $totalDays = null; $normSemester = $this->normalizeSemester($semester); if ($schoolYear !== '' && $normSemester !== '') { $semRange = $this->getSemesterRange($schoolYear, $normSemester); if ($semRange) { try { $events = $this->calendarModel->getEventsBySchoolYearAndSemester($schoolYear, $normSemester); $noSchoolSundays = 0; foreach ($events as $event) { if (!empty($event['no_school'])) { $date = new \DateTime($event['date']); if ($date->format('N') == 7) { if ($date->format('Y-m-d') >= $semRange[0] && $date->format('Y-m-d') <= $semRange[1]) { $noSchoolSundays++; } } } } $totalDays = $this->countSundays($semRange[0], $semRange[1]) - $noSchoolSundays; } catch (\Throwable $e) { log_message('error', 'Failed to count sundays for attendance score: ' . $e->getMessage()); $totalDays = null; } } } if ($totalDays === null) { $totalDays = $this->getConfiguredSemesterLength($semester); } else { $configuredDays = $this->getConfiguredSemesterLength($semester); if ($configuredDays !== null) { $totalDays = $configuredDays; } } if ($absences === null || $totalDays === null || $totalDays < 0) { throw new RuntimeException("Invalid attendance data: could not determine total days or absences for student {$studentId}."); } if ($totalDays == 0) { return ['attendance_score' => 100.0]; } $attendanceRatio = ($totalDays + 1 - $absences) / $totalDays; $score = min(100, max(0, $attendanceRatio * 100)); return ['attendance_score' => round($score, 2)]; } protected function normalizeSemester(?string $input): string { $s = strtolower(trim((string)$input)); if ($s === 'fall' || str_contains($s, '1')) return 'fall'; if ($s === 'spring' || str_contains($s, '2')) return 'spring'; return ''; } protected function getSemesterRange(string $schoolYear, string $semester): ?array { $norm = $this->normalizeSemester($semester); if ($norm === '' || !preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $m)) { // Fallback for single year format if ($norm === '' || !preg_match('/^(\d{4})/', trim($schoolYear), $m)) { return null; } $y1 = (int)$m[1]; $y2 = $y1 + 1; } else { $y1 = (int)$m[1]; $y2 = (int)$m[2]; } $fallStartCfg = (string)($this->configModel->getConfig('fall_semester_start') ?? ''); $fallEndCfg = (string)($this->configModel->getConfig('fall_end_date') ?? ''); $springStartCfg = (string)($this->configModel->getConfig('spring_semester_start') ?? ''); $springEndCfg = (string)($this->configModel->getConfig('last_school_day') ?? ''); if ($norm === 'fall') { $start = ($fallStartCfg !== '') ? sprintf('%04d-%s', $y1, date('m-d', strtotime($fallStartCfg))) : "{$y1}-09-01"; $end = ($fallEndCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($fallEndCfg))) : "{$y2}-01-15"; return [$start, $end]; } if ($norm === 'spring') { $start = ($springStartCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($springStartCfg))) : "{$y2}-01-16"; $end = ($springEndCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($springEndCfg))) : "{$y2}-06-30"; return [$start, $end]; } return null; } private function countSundays(string $startDate, string $endDate): int { $start = new \DateTime($startDate); $end = new \DateTime($endDate); $sundays = 0; $interval = new \DateInterval('P1D'); $period = new \DatePeriod($start, $interval, $end->modify('+1 day')); // include end date foreach ($period as $day) { if ($day->format('N') == 7) { $sundays++; } } return $sundays; } private function getConfiguredSemesterLength(string $semester): ?int { $norm = $this->normalizeSemester($semester); if ($norm === '') { return null; } $configKey = $norm === 'fall' ? 'total_semester1_days' : 'total_semester2_days'; $value = $this->configModel->getConfig($configKey); if (!is_numeric($value)) { return null; } $length = (int) $value; return $length > 0 ? $length : null; } }