package agzam.vyatsu.data.remote import agzam.vyatsu.data.remote.pdf.TextPatterns import agzam.vyatsu.domain.model.Day import agzam.vyatsu.domain.model.Lesson import agzam.vyatsu.domain.model.LessonType import agzam.vyatsu.domain.model.TimeSlot import android.util.Log import org.jsoup.Jsoup import org.jsoup.nodes.Element import java.time.LocalDate import java.time.LocalTime import java.time.format.DateTimeFormatter object TeacherScheduleParser { private const val TAG = "TeacherParserDebug" private val DAY_REGEX = Regex("""^(понедельник|вторник|среда|четверг|пятница|суббота|воскресенье)\s+(\d{2}\.\d{2}\.\d{2})$""") private val INTERVAL_REGEX = Regex("""^(\d{2}:\d{2})-(\d{2}:\d{2})$""") private val GROUP_TOKEN = Regex("""(?, val days: List) /** * Столбец расписания преподавателя: имя и абсолютный индекс ячейки в шапке. * Индекс нужен для точного выравнивания с рядами данных — в шапке могут * присутствовать пустые колонки-разделители, которые не должны сдвигать * соответствие «преподаватель → столбец». */ private data class Column(val teacher: String, val headerIndex: Int) fun parse(html: String): ParsedTeacherSchedule { val doc = Jsoup.parse(html) var headerFound = false var columns: List = emptyList() var teachers: List = emptyList() val days = mutableListOf() var currentDate: LocalDate? = null var currentIntervals: MutableList? = null fun flushDay() { val date = currentDate ?: return val intervals = currentIntervals ?: return if (intervals.isNotEmpty()) days.add(Day(date, intervals.toList())) } for (tr in doc.select("tr")) { val tds = tr.children().filter { it.tagName() == "td" } if (tds.size < 2) continue val first = cellText(tds[0]) val second = cellText(tds[1]) if (!headerFound && first == "День" && second == "Интервал") { columns = tds.mapIndexedNotNull { index, td -> if (index < 2) return@mapIndexedNotNull null val name = cellText(td.selectFirst("span") ?: td) if (name.isEmpty()) null else Column(name, index) }.distinctBy { it.teacher } teachers = columns.map { it.teacher } headerFound = true continue } val dayMatch = DAY_REGEX.matchEntire(first) val intervalMatch = INTERVAL_REGEX.matchEntire(if (dayMatch != null) second else first) if (dayMatch != null) { flushDay() currentDate = LocalDate.parse(dayMatch.groupValues[2], DATE_FMT) currentIntervals = mutableListOf() } if (intervalMatch != null && currentIntervals != null) { parseIntervalRow(tds, intervalMatch, dayMatch != null, columns, currentIntervals!!) } } flushDay() Log.d(TAG, "parse: teachers=$teachers days=${days.size}") for (day in days) { Log.d(TAG, " parse ${day.date}: intervals=${day.intervals.size}") for (slot in day.intervals) { val mel = slot.lessons.filter { it.teacher == "Мельцов В.Ю." } val teachersInSlot = slot.lessons.map { it.teacher }.distinct() Log.d( TAG, " slot ${slot.start}-${slot.end} total=${slot.lessons.size} meltsov=${mel.size} teachers=$teachersInSlot" ) mel.forEach { l -> Log.d(TAG, " mel: room=${l.room} group=${l.group} subj=${l.subject} type=${l.type} raw=${l.raw}") } } } return ParsedTeacherSchedule(teachers, days) } private fun parseIntervalRow( tds: List, intervalMatch: MatchResult, dayRow: Boolean, columns: List, intervals: MutableList ) { val start = LocalTime.parse(intervalMatch.groupValues[1]) val end = LocalTime.parse(intervalMatch.groupValues[2]) val lessons = mutableListOf() for (column in columns) { // Ряд с днём содержит обе ведущие ячейки (День, Интервал) и совпадает // по индексам с шапкой; ряд только с интервалом пропускает ячейку дня // (rowspan) — его ячейки сдвинуты на одну позицию влево. val dataIndex = if (dayRow) column.headerIndex else column.headerIndex - 1 val cell = tds.getOrNull(dataIndex) ?: continue lessons += parseCell(cell, column.teacher) } if (lessons.isNotEmpty()) intervals.add(TimeSlot(start, end, lessons)) } private fun parseCell(cell: Element, teacher: String): List { val raw = cellText(cell) if (raw.isEmpty()) return emptyList() val room = TextPatterns.ROOM_REGEX.find(raw)?.value val typeMatch = TextPatterns.LESSON_TYPE_REGEX.find(raw) val type = LessonType.fromRaw(typeMatch?.value?.trim() ?: "") val segments = raw.split(SPLIT_BY_GROUP).map { it.trim() }.filter { it.isNotEmpty() } val groupSegments = segments.filter { GROUP_TOKEN.containsMatchIn(it) } if (groupSegments.isEmpty()) { return listOf( Lesson( raw = raw, room = room, subject = subjectOf(raw, room, typeMatch), type = type, teacher = teacher ) ) } val subject = subjectOf(raw, room, typeMatch) return groupSegments.flatMap { segment -> val groups = GROUP_TOKEN.findAll(segment).map { it.value }.distinct().toList() val subgroup = TextPatterns.SUBGROUP_REGEX.find(segment)?.value groups.map { group -> Lesson( raw = segment, room = room, group = group, subgroup = subgroup, subject = subject, type = type, teacher = teacher ) } } } private fun subjectOf(raw: String, room: String?, typeMatch: MatchResult?): String? { val end = typeMatch?.range?.first ?: GROUP_TOKEN.find(raw)?.range?.first ?: raw.length val prefix = raw.substring(0, end).trim() val stripped = room?.let { prefix.removePrefix(it).trim() } ?: prefix return stripped.ifEmpty { null } } private fun cellText(el: Element): String = el.text().replace('\u00A0', ' ').trim() }