feat(domain): add teacher models and repository interfaces
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user