forked from agzam/vyatsu-schedule
refactor: database multi-period schedule storage
This commit is contained in:
@@ -5,6 +5,7 @@ import agzam.vyatsu.data.local.GroupEntity
|
||||
import agzam.vyatsu.data.remote.GroupsParser
|
||||
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
||||
import agzam.vyatsu.data.remote.VyatsuUrls
|
||||
import agzam.vyatsu.domain.ScheduleDates
|
||||
import agzam.vyatsu.domain.model.Group
|
||||
import agzam.vyatsu.domain.model.Season
|
||||
import agzam.vyatsu.domain.repository.GroupRepository
|
||||
@@ -50,16 +51,18 @@ class GroupRepositoryImpl(
|
||||
id = group.id,
|
||||
name = group.name,
|
||||
faculty = group.faculty,
|
||||
autumnFirst = autumn?.first ?: "",
|
||||
autumnSecond = autumn?.second ?: "",
|
||||
springFirst = spring?.first ?: "",
|
||||
springSecond = spring?.second ?: "",
|
||||
autumnFirst = autumn?.first?.let(::epochDay),
|
||||
autumnSecond = autumn?.second?.let(::epochDay),
|
||||
springFirst = spring?.first?.let(::epochDay),
|
||||
springSecond = spring?.second?.let(::epochDay),
|
||||
lastSyncedAt = now()
|
||||
)
|
||||
}
|
||||
groupDao.insertAll(entities)
|
||||
}
|
||||
|
||||
private fun epochDay(dateStr: String): Long = ScheduleDates.asDate(dateStr).toEpochDay()
|
||||
|
||||
private fun now(): Long = System.currentTimeMillis()
|
||||
|
||||
private fun GroupEntity.toDomain(): Group = Group(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package agzam.vyatsu.data.repository
|
||||
|
||||
import agzam.vyatsu.data.local.ScheduleCodec
|
||||
import agzam.vyatsu.data.local.DayEntity
|
||||
import agzam.vyatsu.data.local.LessonEntity
|
||||
import agzam.vyatsu.data.local.ScheduleDao
|
||||
import agzam.vyatsu.data.local.ScheduleEntity
|
||||
import agzam.vyatsu.data.local.GroupDao
|
||||
@@ -9,39 +10,50 @@ import agzam.vyatsu.data.remote.pdf.PdfScheduleParser
|
||||
import agzam.vyatsu.data.remote.VyatsuHttpClient
|
||||
import agzam.vyatsu.data.remote.VyatsuUrls
|
||||
import agzam.vyatsu.domain.ScheduleDates
|
||||
import agzam.vyatsu.domain.model.Day
|
||||
import agzam.vyatsu.domain.model.DateRange
|
||||
import agzam.vyatsu.domain.model.Lesson
|
||||
import agzam.vyatsu.domain.model.Schedule
|
||||
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||
import agzam.vyatsu.domain.model.Season
|
||||
import agzam.vyatsu.domain.model.TimeSlot
|
||||
import agzam.vyatsu.domain.repository.ScheduleRepository
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
class ScheduleRepositoryImpl(
|
||||
private val scheduleDao: ScheduleDao,
|
||||
private val groupDao: GroupDao,
|
||||
private val codec: ScheduleCodec
|
||||
private val groupDao: GroupDao
|
||||
) : ScheduleRepository {
|
||||
|
||||
override fun observeSchedule(groupId: String): Flow<Schedule?> =
|
||||
scheduleDao.observe(groupId).map { it?.toDomain() }
|
||||
combine(
|
||||
scheduleDao.observeSchedules(groupId),
|
||||
scheduleDao.observeDaysForGroup(groupId),
|
||||
scheduleDao.observeLessonsForGroup(groupId)
|
||||
) { schedules, days, lessons ->
|
||||
buildSchedule(schedules, days, lessons)
|
||||
}
|
||||
|
||||
override suspend fun getSchedule(groupId: String): Schedule? =
|
||||
scheduleDao.get(groupId)?.toDomain()
|
||||
buildSchedule(
|
||||
scheduleDao.getSchedules(groupId),
|
||||
scheduleDao.getDaysForGroup(groupId),
|
||||
scheduleDao.getLessonsForGroup(groupId)
|
||||
)
|
||||
|
||||
override suspend fun sync(groupId: String): Schedule? {
|
||||
Log.d(TAG, "sync(groupId=$groupId) start")
|
||||
val group = groupDao.getById(groupId)
|
||||
if (group == null) {
|
||||
val group = groupDao.getById(groupId) ?: run {
|
||||
Log.w(TAG, "sync: group $groupId not found in DB")
|
||||
return null
|
||||
}
|
||||
val season = ScheduleDates.currentSeason()
|
||||
val range = dateRangeFor(group, season)
|
||||
if (range == null) {
|
||||
Log.w(TAG, "sync: no date range for season $season (group ${group.id}, " +
|
||||
"autumn=[${group.autumnFirst}, ${group.autumnSecond}], " +
|
||||
"spring=[${group.springFirst}, ${group.springSecond}])")
|
||||
val range = dateRangeFor(group, season) ?: run {
|
||||
Log.w(TAG, "sync: no date range for season $season (group ${group.id})")
|
||||
return null
|
||||
}
|
||||
Log.d(TAG, "sync: season=$season range=[${range.first}, ${range.second}]")
|
||||
@@ -55,17 +67,26 @@ class ScheduleRepositoryImpl(
|
||||
val days = PdfScheduleParser.parse(bytes)
|
||||
Log.d(TAG, "sync: parsed ${days.size} days")
|
||||
|
||||
val entity = ScheduleEntity(
|
||||
groupId = groupId,
|
||||
season = season.name,
|
||||
dateRangeFirst = range.first,
|
||||
dateRangeSecond = range.second,
|
||||
weeksJson = codec.toJson(days),
|
||||
lastSyncedAt = now()
|
||||
val weekStart = ScheduleDates.asDate(range.first).toEpochDay()
|
||||
val weekEnd = ScheduleDates.asDate(range.second).toEpochDay()
|
||||
|
||||
val existing = scheduleDao.getSchedules(groupId)
|
||||
.firstOrNull { it.season == season && it.weekStart == weekStart }
|
||||
val scheduleId = if (existing != null) existing.id
|
||||
else scheduleDao.insertSchedule(
|
||||
ScheduleEntity(
|
||||
groupId = groupId,
|
||||
season = season,
|
||||
weekStart = weekStart,
|
||||
weekEnd = weekEnd,
|
||||
lastSyncedAt = now()
|
||||
)
|
||||
)
|
||||
scheduleDao.upsert(entity)
|
||||
Log.d(TAG, "sync: upserted schedule for $groupId")
|
||||
entity.toDomain()
|
||||
replaceDays(scheduleId, days)
|
||||
|
||||
scheduleDao.trim(groupId, MAX_PERIODS)
|
||||
|
||||
getSchedule(groupId)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "sync: failed for $groupId (${e::class.simpleName}: ${e.message})", e)
|
||||
null
|
||||
@@ -73,33 +94,126 @@ class ScheduleRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun isStale(groupId: String): Boolean {
|
||||
val entity = scheduleDao.get(groupId) ?: return true
|
||||
return now() - entity.lastSyncedAt > STALE_WINDOW_MS
|
||||
val schedules = scheduleDao.getSchedules(groupId)
|
||||
val latest = schedules.maxByOrNull { it.weekStart } ?: return true
|
||||
return now() - latest.lastSyncedAt > STALE_WINDOW_MS
|
||||
}
|
||||
|
||||
override suspend fun clear(groupId: String) = scheduleDao.delete(groupId)
|
||||
override suspend fun clearPeriod(groupId: String, period: SchedulePeriod) {
|
||||
scheduleDao.deleteSchedule(
|
||||
groupId = groupId,
|
||||
season = period.season.name,
|
||||
weekStart = period.weekStart.toEpochDay()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearAll(groupId: String) = scheduleDao.deleteAll(groupId)
|
||||
|
||||
private suspend fun replaceDays(scheduleId: Long, days: List<Day>) {
|
||||
scheduleDao.deleteDaysForSchedule(scheduleId)
|
||||
val dayEntities = days.map { DayEntity(scheduleId = scheduleId, date = it.date.toEpochDay()) }
|
||||
val dayIdByDate = mutableMapOf<LocalDate, Long>()
|
||||
scheduleDao.insertDays(dayEntities)
|
||||
for (day in dayEntities) {
|
||||
dayIdByDate[LocalDate.ofEpochDay(day.date)] = day.id
|
||||
}
|
||||
val lessons = days.flatMap { day ->
|
||||
day.intervals.flatMapIndexed { slotIndex, slot ->
|
||||
slot.lessons.mapIndexed { lessonIndex, lesson ->
|
||||
LessonEntity(
|
||||
dayId = dayIdByDate.getValue(day.date),
|
||||
date = day.date.toEpochDay(),
|
||||
slotIndex = slotIndex,
|
||||
start = slot.start.toMinutesOfDay(),
|
||||
end = slot.end.toMinutesOfDay(),
|
||||
lessonIndex = lessonIndex,
|
||||
raw = lesson.raw,
|
||||
room = lesson.room,
|
||||
groupName = lesson.group,
|
||||
subgroup = lesson.subgroup,
|
||||
subject = lesson.subject,
|
||||
type = lesson.type,
|
||||
teacher = lesson.teacher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
scheduleDao.insertLessons(lessons)
|
||||
}
|
||||
|
||||
private fun buildSchedule(
|
||||
schedules: List<ScheduleEntity>,
|
||||
dayEntities: List<DayEntity>,
|
||||
lessonEntities: List<LessonEntity>
|
||||
): Schedule? {
|
||||
if (schedules.isEmpty()) return null
|
||||
val groupId = schedules.first().groupId
|
||||
|
||||
val periods = schedules.sortedBy { it.weekStart }.map {
|
||||
SchedulePeriod(
|
||||
weekStart = LocalDate.ofEpochDay(it.weekStart),
|
||||
weekEnd = LocalDate.ofEpochDay(it.weekEnd),
|
||||
season = it.season
|
||||
)
|
||||
}
|
||||
|
||||
val lessonsByDay = lessonEntities.groupBy { it.dayId }
|
||||
val days = dayEntities
|
||||
.sortedBy { it.date }
|
||||
.map { day ->
|
||||
val lessons = lessonsByDay[day.id].orEmpty()
|
||||
.groupBy { it.slotIndex }
|
||||
.toSortedMap()
|
||||
.map { (_, slotLessons) ->
|
||||
TimeSlot(
|
||||
start = LocalTime.of(slotLessons.first().start / 60, slotLessons.first().start % 60),
|
||||
end = LocalTime.of(slotLessons.first().end / 60, slotLessons.first().end % 60),
|
||||
lessons = slotLessons
|
||||
.sortedBy { it.lessonIndex }
|
||||
.map { it.toDomain() }
|
||||
)
|
||||
}
|
||||
Day(date = LocalDate.ofEpochDay(day.date), intervals = lessons)
|
||||
}
|
||||
|
||||
return Schedule(groupId = groupId, periods = periods, days = days)
|
||||
}
|
||||
|
||||
private fun LessonEntity.toDomain(): Lesson = Lesson(
|
||||
raw = raw,
|
||||
room = room,
|
||||
group = groupName,
|
||||
subgroup = subgroup,
|
||||
subject = subject,
|
||||
type = type,
|
||||
teacher = teacher
|
||||
)
|
||||
|
||||
private fun dateRangeFor(group: GroupEntity, season: Season): DateRange? {
|
||||
return when (season) {
|
||||
Season.AUTUMN -> if (group.autumnFirst.isNotEmpty() && group.autumnSecond.isNotEmpty()) {
|
||||
DateRange(group.autumnFirst, group.autumnSecond)
|
||||
Season.AUTUMN -> if (group.autumnFirst != null && group.autumnSecond != null) {
|
||||
DateRange(
|
||||
LocalDate.ofEpochDay(group.autumnFirst).format(RANGE_FMT),
|
||||
LocalDate.ofEpochDay(group.autumnSecond).format(RANGE_FMT)
|
||||
)
|
||||
} else null
|
||||
Season.SPRING -> if (group.springFirst.isNotEmpty() && group.springSecond.isNotEmpty()) {
|
||||
DateRange(group.springFirst, group.springSecond)
|
||||
Season.SPRING -> if (group.springFirst != null && group.springSecond != null) {
|
||||
DateRange(
|
||||
LocalDate.ofEpochDay(group.springFirst).format(RANGE_FMT),
|
||||
LocalDate.ofEpochDay(group.springSecond).format(RANGE_FMT)
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
private fun now(): Long = System.currentTimeMillis()
|
||||
private fun LocalTime.toMinutesOfDay(): Int = hour * 60 + minute
|
||||
|
||||
private fun ScheduleEntity.toDomain(): Schedule = Schedule(
|
||||
groupId = groupId,
|
||||
season = Season.valueOf(season),
|
||||
days = codec.fromJson(weeksJson)
|
||||
)
|
||||
private fun now(): Long = System.currentTimeMillis()
|
||||
|
||||
companion object {
|
||||
private const val STALE_WINDOW_MS = 12 * 60 * 60 * 1000L
|
||||
private const val MAX_PERIODS = 3
|
||||
private const val TAG = "ScheduleRepo"
|
||||
private val RANGE_FMT = java.time.format.DateTimeFormatter.ofPattern("ddMMyyyy")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user