feat(domain): add teacher models and repository interfaces

This commit is contained in:
2026-09-19 01:43:17 +03:00
parent 1077be1897
commit 17d437aace
9 changed files with 255 additions and 0 deletions
@@ -0,0 +1,7 @@
package agzam.vyatsu.domain.model
data class BulkSyncProgress(
val total: Int,
val done: Int,
val failed: Int
)
@@ -0,0 +1,11 @@
package agzam.vyatsu.domain.model
/**
* Кафедра ВятГУ.
* departmentId — идентификатор кафедры из data-kaf_period_id без последней цифры (как в URL).
*/
data class Department(
val departmentId: String,
val name: String,
val faculty: String
)
@@ -0,0 +1,6 @@
package agzam.vyatsu.domain.model
data class ParsedDepartments(
val departments: List<Department>,
val dateRanges: Map<String, Map<Season, List<DateRange>>>
)
@@ -0,0 +1,11 @@
package agzam.vyatsu.domain.model
/**
* Преподаватель ВятГУ.
* Один и тот же преподаватель может вести занятия в нескольких кафедрах,
* поэтому модель не привязана к departmentId — расписание агрегируется
* по всем кафедрам периода.
*/
data class Teacher(
val name: String
)
@@ -0,0 +1,57 @@
package agzam.vyatsu.domain.model
import java.time.LocalDate
import java.time.LocalTime
/**
* Расписание преподавателя за период, агрегированное по всем кафедрам.
*/
data class TeacherSchedule(
val period: SchedulePeriod,
val days: List<Day>
) {
/**
* Возвращает дни периода только с занятиями указанного преподавателя.
* Слоты без занятий отбрасываются, порядок [Day] и [TimeSlot] сохраняется.
*
* Дедупликация — ТОЛЬКО по полному совпадению (дата + слот + аудитория +
* группа + предмет + тип + raw): два занятия коллапсируют в одно лишь когда
* они идентичны целиком (одно физическое занятие, прочитанное из файлов двух
* кафедр). Всё, что отличается хотя бы одним полем, остаётся и показывается.
*/
fun daysForTeacher(teacherName: String): List<Day> =
days.mapNotNull { day ->
val seen = HashSet<FullMatchKey>()
val intervals = day.intervals.mapNotNull { slot ->
val lessons = slot.lessons
.filter { it.teacher == teacherName }
.filter {
seen.add(
FullMatchKey(
date = day.date,
start = slot.start,
end = slot.end,
room = it.room,
group = it.group,
subject = it.subject,
type = it.type,
raw = it.raw
)
)
}
if (lessons.isEmpty()) null else slot.copy(lessons = lessons)
}
if (intervals.isEmpty()) null else day.copy(intervals = intervals)
}
private data class FullMatchKey(
val date: LocalDate,
val start: LocalTime,
val end: LocalTime,
val room: String?,
val group: String?,
val subject: String?,
val type: LessonType,
val raw: String
)
}
@@ -9,6 +9,14 @@ interface PreferencesRepository {
suspend fun selectedDayDate(): String?
suspend fun setSelectedDayDate(date: String)
suspend fun selectedTeacherId(): String? = null
suspend fun setSelectedTeacherId(id: String) = Unit
suspend fun selectedTeacherWeekStart(): String? = null
suspend fun setSelectedTeacherWeekStart(weekStart: String) = Unit
suspend fun selectedTeacherDayDate(): String? = null
suspend fun setSelectedTeacherDayDate(date: String) = Unit
suspend fun clearSelectedTeacher() = Unit
suspend fun autoUpdateEnabled(): Boolean
suspend fun setAutoUpdateEnabled(enabled: Boolean)
@@ -0,0 +1,12 @@
package agzam.vyatsu.domain.repository
import agzam.vyatsu.domain.model.Department
import kotlinx.coroutines.flow.Flow
interface TeacherRepository {
fun observeDepartments(): Flow<List<Department>>
suspend fun getDepartments(): List<Department>
suspend fun getFaculties(): List<String>
suspend fun refreshIfStale(): Boolean
suspend fun forceRefresh(): Boolean
}
@@ -0,0 +1,16 @@
package agzam.vyatsu.domain.repository
import agzam.vyatsu.domain.model.BulkSyncProgress
import agzam.vyatsu.domain.model.SchedulePeriod
import agzam.vyatsu.domain.model.Teacher
import agzam.vyatsu.domain.model.TeacherSchedule
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
interface TeacherScheduleRepository {
fun observeTeachers(period: SchedulePeriod): Flow<List<Teacher>>
fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?>
suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress>
suspend fun hasData(period: SchedulePeriod): Boolean
suspend fun clearAll()
}
@@ -0,0 +1,127 @@
package agzam.vyatsu
import agzam.vyatsu.domain.model.Day
import agzam.vyatsu.domain.model.Lesson
import agzam.vyatsu.domain.model.LessonType
import agzam.vyatsu.domain.model.SchedulePeriod
import agzam.vyatsu.domain.model.Season
import agzam.vyatsu.domain.model.TeacherSchedule
import agzam.vyatsu.domain.model.TimeSlot
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate
import java.time.LocalTime
class TeacherScheduleDomainTest {
@Test
fun daysForTeacherFiltersAndPreservesOrder() {
val date1 = LocalDate.of(2026, 9, 14)
val date2 = LocalDate.of(2026, 9, 15)
val slot1 = TimeSlot(
LocalTime.of(8, 20),
LocalTime.of(9, 50),
listOf(lesson(room = "4-302", teacher = "A"), lesson(room = "5-100", teacher = "B"))
)
val slot2 = TimeSlot(
LocalTime.of(10, 0),
LocalTime.of(11, 30),
listOf(lesson(room = "5-100", teacher = "B"))
)
val day2Slot = TimeSlot(
LocalTime.of(8, 20),
LocalTime.of(9, 50),
listOf(lesson(room = "5-100", teacher = "B"))
)
val schedule = TeacherSchedule(
period = SchedulePeriod(date1, date2, Season.AUTUMN),
days = listOf(Day(date1, listOf(slot1, slot2)), Day(date2, listOf(day2Slot)))
)
val result = schedule.daysForTeacher("A")
assertEquals(1, result.size)
assertEquals(date1, result[0].date)
assertEquals(1, result[0].intervals.size)
assertEquals(LocalTime.of(8, 20), result[0].intervals[0].start)
assertEquals(1, result[0].intervals[0].lessons.size)
assertEquals("A", result[0].intervals[0].lessons[0].teacher)
}
@Test
fun daysForTeacherDropsDayWithoutTeacher() {
val date1 = LocalDate.of(2026, 9, 14)
val date2 = LocalDate.of(2026, 9, 15)
val schedule = TeacherSchedule(
period = SchedulePeriod(date1, date2, Season.AUTUMN),
days = listOf(
Day(date1, listOf(slotOf(lesson(room = "4-302", teacher = "A")))),
Day(date2, listOf(slotOf(lesson(room = "5-100", teacher = "B"))))
)
)
val result = schedule.daysForTeacher("A")
assertEquals(1, result.size)
assertEquals(date1, result[0].date)
}
@Test
fun dedupCollapsesOnlyIdenticalLessons() {
val date = LocalDate.of(2026, 9, 14)
val raw = "4-302 Программирование Лекция ИВТб-3304-01-00"
val identical = lesson(
room = "4-302",
group = "ИВТб-3304-01-00",
teacher = "A",
raw = raw,
subject = "Программирование"
)
val differsInGroup = identical.copy(group = "ИВТб-3304-02-00")
val differsInRoom = identical.copy(room = "5-102")
val schedule = TeacherSchedule(
period = SchedulePeriod(date, date, Season.AUTUMN),
days = listOf(
Day(
date,
listOf(
TimeSlot(
LocalTime.of(8, 20),
LocalTime.of(9, 50),
listOf(identical, identical, differsInGroup, differsInRoom)
)
)
)
)
)
val result = schedule.daysForTeacher("A")
val lessons = result[0].intervals[0].lessons
assertEquals(3, lessons.size)
assertEquals(1, lessons.count { it == identical })
assertEquals(1, lessons.count { it.room == "4-302" && it.group == "ИВТб-3304-01-00" })
assertTrue(lessons.any { it.group == "ИВТб-3304-02-00" })
assertTrue(lessons.any { it.room == "5-102" })
}
private fun slotOf(lesson: Lesson) =
TimeSlot(LocalTime.of(8, 20), LocalTime.of(9, 50), listOf(lesson))
private fun lesson(
room: String,
group: String = "ИВТб-3304-01-00",
teacher: String,
raw: String = "$room Программирование Лекция $group",
subject: String = "Программирование"
) = Lesson(
raw = raw,
room = room,
group = group,
subject = subject,
type = LessonType.LECTURE,
teacher = teacher
)
}