diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 0bd971ec..36c3c4f8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester @@ -33,81 +34,162 @@ class GradeAverageProvider @Inject constructor( private val minusModifier get() = preferencesRepository.gradeMinusModifier - fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = flowWithResourceIn { - val semesters = semesterRepository.getSemesters(student) + fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = + flowWithResourceIn { + val semesters = semesterRepository.getSemesters(student) - when (preferencesRepository.gradeAverageMode) { - ONE_SEMESTER -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh) - BOTH_SEMESTERS -> calculateBothSemestersAverage(student, semesters, semesterId, forceRefresh) - ALL_YEAR -> calculateAllYearAverage(student, semesters, semesterId, forceRefresh) - } - }.distinctUntilChanged() - - private fun calculateBothSemestersAverage(student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean): Flow>> { - val selectedSemester = semesters.single { it.semesterId == semesterId } - val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } - - val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh) - - return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else { - val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh) - selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails -> - val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 } - secondDetails.copy(data = selectedDetails.data?.map { selected -> - val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject } - selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { - val selectedGrades = selected.grades.updateModifiers(student).calcAverage() - (selectedGrades + (second?.grades?.updateModifiers(student)?.calcAverage() ?: selectedGrades)) / 2 - } else (selected.average + (second?.average ?: selected.average)) / 2) - }) - } - } - } - - private fun calculateAllYearAverage(student: Student, semesters: List, semesterId: Int, forceRefresh: Boolean): Flow>> { - val selectedSemester = semesters.single { it.semesterId == semesterId } - val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } - - val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh) - - return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else { - val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh) - selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails -> - val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 } - secondDetails.copy(data = selectedDetails.data?.map { selected -> - val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject } - selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { - (selected.grades.updateModifiers(student) + second?.grades?.updateModifiers(student).orEmpty()).calcAverage() - } else selected.average) - }) - } - } - } - - private fun getSemesterDetailsWithAverage(student: Student, semester: Semester, forceRefresh: Boolean): Flow>> { - return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh).map { res -> - val (details, summaries) = res.data ?: null to null - val isAnyAverage = summaries.orEmpty().any { it.average != .0 } - val allGrades = details.orEmpty().groupBy { it.subject } - - val items = summaries?.emulateEmptySummaries(student, semester, allGrades.toList(), isAnyAverage)?.map { summary -> - val grades = allGrades[summary.subject].orEmpty() - GradeDetailsWithAverage( - subject = summary.subject, - average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) { - grades.updateModifiers(student).calcAverage() - } else summary.average, - points = summary.pointsSum, - summary = summary, - grades = grades + when (preferencesRepository.gradeAverageMode) { + ONE_SEMESTER -> getGradeSubjects( + student = student, + semester = semesters.single { it.semesterId == semesterId }, + forceRefresh = forceRefresh + ) + BOTH_SEMESTERS -> calculateCombinedAverage( + student = student, + semesters = semesters, + semesterId = semesterId, + forceRefresh = forceRefresh, + averageMode = BOTH_SEMESTERS + ) + ALL_YEAR -> calculateCombinedAverage( + student = student, + semesters = semesters, + semesterId = semesterId, + forceRefresh = forceRefresh, + averageMode = ALL_YEAR ) } + }.distinctUntilChanged() - Resource(res.status, items, res.error) + private fun calculateCombinedAverage( + student: Student, + semesters: List, + semesterId: Int, + forceRefresh: Boolean, + averageMode: GradeAverageMode + ): Flow>> { + val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc + val selectedSemester = semesters.single { it.semesterId == semesterId } + val firstSemester = + semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } + + val selectedSemesterGradeSubjects = + getGradeSubjects(student, selectedSemester, forceRefresh) + + if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects + + val firstSemesterGradeSubjects = + getGradeSubjects(student, firstSemester, forceRefresh) + + return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject -> + if (firstSemesterGradeSubject.status == Status.ERROR) { + return@combine firstSemesterGradeSubject + } + + val isAnyAverage = secondSemesterGradeSubject.data.orEmpty().any { it.average != .0 } + val updatedData = secondSemesterGradeSubject.data?.map { secondSemesterSubject -> + val firstSemesterSubject = firstSemesterGradeSubject.data.orEmpty() + .singleOrNull { it.subject == secondSemesterSubject.subject } + + val updatedAverage = if (averageMode == ALL_YEAR) { + calculateAllYearAverage( + student = student, + isAnyAverage = isAnyAverage, + gradeAverageForceCalc = gradeAverageForceCalc, + secondSemesterSubject = secondSemesterSubject, + firstSemesterSubject = firstSemesterSubject + ) + } else { + calculateBothSemestersAverage( + student = student, + isAnyAverage = isAnyAverage, + gradeAverageForceCalc = gradeAverageForceCalc, + secondSemesterSubject = secondSemesterSubject, + firstSemesterSubject = firstSemesterSubject + ) + } + secondSemesterSubject.copy(average = updatedAverage) + } + secondSemesterGradeSubject.copy(data = updatedData) } } - private fun List.emulateEmptySummaries(student: Student, semester: Semester, grades: List>>, calcAverage: Boolean): List { + private fun calculateAllYearAverage( + student: Student, + isAnyAverage: Boolean, + gradeAverageForceCalc: Boolean, + secondSemesterSubject: GradeSubject, + firstSemesterSubject: GradeSubject? + ) = if (!isAnyAverage || gradeAverageForceCalc) { + val updatedSecondSemesterGrades = + secondSemesterSubject.grades.updateModifiers(student) + val updatedFirstSemesterGrades = + firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() + + (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage() + } else { + secondSemesterSubject.average + } + + private fun calculateBothSemestersAverage( + student: Student, + isAnyAverage: Boolean, + gradeAverageForceCalc: Boolean, + secondSemesterSubject: GradeSubject, + firstSemesterSubject: GradeSubject? + ) = if (!isAnyAverage || gradeAverageForceCalc) { + val secondSemesterAverage = + secondSemesterSubject.grades.updateModifiers(student).calcAverage() + val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) + ?.calcAverage() ?: secondSemesterAverage + val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1 + + (secondSemesterAverage + firstSemesterAverage) / divider + } else { + (secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / 2 + } + + private fun getGradeSubjects( + student: Student, + semester: Semester, + forceRefresh: Boolean + ): Flow>> { + val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc + + return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) + .map { res -> + val (details, summaries) = res.data ?: null to null + val isAnyAverage = summaries.orEmpty().any { it.average != .0 } + val allGrades = details.orEmpty().groupBy { it.subject } + + val items = summaries?.emulateEmptySummaries( + student, + semester, + allGrades.toList(), + isAnyAverage + )?.map { summary -> + val grades = allGrades[summary.subject].orEmpty() + GradeSubject( + subject = summary.subject, + average = if (!isAnyAverage || gradeAverageForceCalc) { + grades.updateModifiers(student).calcAverage() + } else summary.average, + points = summary.pointsSum, + summary = summary, + grades = grades + ) + } + + Resource(res.status, items, res.error) + } + } + + private fun List.emulateEmptySummaries( + student: Student, + semester: Semester, + grades: List>>, + calcAverage: Boolean + ): List { if (isNotEmpty() && size > grades.size) return this return grades.mapIndexed { i, (subject, details) -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeDetailsWithAverage.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt similarity index 88% rename from app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeDetailsWithAverage.kt rename to app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt index 3f5d706b..ee4266c5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeDetailsWithAverage.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.modules.grade import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary -data class GradeDetailsWithAverage( +data class GradeSubject( val subject: String, val average: Double, val points: String, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt index afec6b5e..6d86c7bb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -10,9 +10,9 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider -import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE +import io.github.wulkanowy.ui.modules.grade.GradeSubject import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource @@ -201,8 +201,9 @@ class GradeDetailsPresenter @Inject constructor( }.launch() } - private fun updateNewGradesAmount(grades: List) { - newGradesAmount = grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } } + private fun updateNewGradesAmount(grades: List) { + newGradesAmount = + grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } } } private fun showErrorViewOnError(message: String, error: Throwable) { @@ -217,7 +218,7 @@ class GradeDetailsPresenter @Inject constructor( } @SuppressLint("DefaultLocale") - private fun createGradeItems(items: List): List { + private fun createGradeItems(items: List): List { return items .let { gradesWithAverages -> if (!preferencesRepository.showSubjectsWithoutGrades) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt index 17c14b85..7adfd7e5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -6,7 +6,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider -import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage +import io.github.wulkanowy.ui.modules.grade.GradeSubject import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResourceIn @@ -135,14 +135,14 @@ class GradeSummaryPresenter @Inject constructor( cancelJobs("load") } - private fun createGradeSummaryItems(items: List): List { + private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } .sortedBy { it.subject } .map { it.summary.copy(average = it.average) } } - private fun checkEmpty(gradeSummary: GradeDetailsWithAverage): Boolean { + private fun checkEmpty(gradeSummary: GradeSubject): Boolean { return gradeSummary.run { summary.finalGrade.isBlank() && summary.predictedGrade.isBlank() diff --git a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt index c57b6247..9aa4fcac 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt @@ -16,63 +16,58 @@ fun List.calcAverage(): Double { } @JvmName("calcSummaryAverage") -fun List.calcAverage(): Double { - return asSequence().mapNotNull { - if (it.finalGrade.matches("[0-6]".toRegex())) it.finalGrade.toDouble() else null - }.average().let { if (it.isNaN()) 0.0 else it } -} - -fun Grade.getBackgroundColor(theme: String): Int { - return when (theme) { - "grade_color" -> getGradeColor() - "material" -> when (value.toInt()) { - 6 -> R.color.grade_material_six - 5 -> R.color.grade_material_five - 4 -> R.color.grade_material_four - 3 -> R.color.grade_material_three - 2 -> R.color.grade_material_two - 1 -> R.color.grade_material_one - else -> R.color.grade_material_default - } - else -> when (value.toInt()) { - 6 -> R.color.grade_vulcan_six - 5 -> R.color.grade_vulcan_five - 4 -> R.color.grade_vulcan_four - 3 -> R.color.grade_vulcan_three - 2 -> R.color.grade_vulcan_two - 1 -> R.color.grade_vulcan_one - else -> R.color.grade_vulcan_default - } +fun List.calcAverage() = asSequence() + .mapNotNull { + if (it.finalGrade.matches("[0-6]".toRegex())) { + it.finalGrade.toDouble() + } else null } -} + .average() + .let { if (it.isNaN()) 0.0 else it } -fun Grade.getGradeColor(): Int { - return when (color) { - "000000" -> R.color.grade_black - "F04C4C" -> R.color.grade_red - "20A4F7" -> R.color.grade_blue - "6ECD07" -> R.color.grade_green - "B16CF1" -> R.color.grade_purple +fun Grade.getBackgroundColor(theme: String) = when (theme) { + "grade_color" -> getGradeColor() + "material" -> when (value.toInt()) { + 6 -> R.color.grade_material_six + 5 -> R.color.grade_material_five + 4 -> R.color.grade_material_four + 3 -> R.color.grade_material_three + 2 -> R.color.grade_material_two + 1 -> R.color.grade_material_one else -> R.color.grade_material_default } + else -> when (value.toInt()) { + 6 -> R.color.grade_vulcan_six + 5 -> R.color.grade_vulcan_five + 4 -> R.color.grade_vulcan_four + 3 -> R.color.grade_vulcan_three + 2 -> R.color.grade_vulcan_two + 1 -> R.color.grade_vulcan_one + else -> R.color.grade_vulcan_default + } +} + +fun Grade.getGradeColor() = when (color) { + "000000" -> R.color.grade_black + "F04C4C" -> R.color.grade_red + "20A4F7" -> R.color.grade_blue + "6ECD07" -> R.color.grade_green + "B16CF1" -> R.color.grade_purple + else -> R.color.grade_material_default } inline val Grade.colorStringId: Int - get() { - return when (color) { - "000000" -> R.string.all_black - "F04C4C" -> R.string.all_red - "20A4F7" -> R.string.all_blue - "6ECD07" -> R.string.all_green - "B16CF1" -> R.string.all_purple - else -> R.string.all_empty_color - } + get() = when (color) { + "000000" -> R.string.all_black + "F04C4C" -> R.string.all_red + "20A4F7" -> R.string.all_blue + "6ECD07" -> R.string.all_green + "B16CF1" -> R.string.all_purple + else -> R.string.all_empty_color } -fun Grade.changeModifier(plusModifier: Double, minusModifier: Double): Grade { - return when { - modifier > 0 -> copy(modifier = plusModifier) - modifier < 0 -> copy(modifier = -minusModifier) - else -> this - } +fun Grade.changeModifier(plusModifier: Double, minusModifier: Double) = when { + modifier > 0 -> copy(modifier = plusModifier) + modifier < 0 -> copy(modifier = -minusModifier) + else -> this } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index fed0093e..0da02cf5 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -15,6 +15,7 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.toList @@ -152,6 +153,51 @@ class GradeAverageProviderTest { assertEquals(3.5, items[1].data?.single { it.subject == "Język polski" }!!.average, .0) // from details and after set custom plus/minus } + @Test + fun `calc all year semester average with delayed emit`(){ + every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR + + coEvery { semesterRepository.getSemesters(student) } returns semesters + coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flow { + emit(Resource.loading()) + delay(1000) + emit(Resource.success(secondGradeWithModifier to secondSummariesWithModifier)) + } + coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flow { + emit(Resource.loading()) + emit(Resource.success(secondGradeWithModifier to secondSummariesWithModifier)) + } + + val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, false).toList() } + + with(items[0]) { + assertEquals(Status.LOADING, status) + assertEquals(null, data) + } + with(items[1]) { + assertEquals(Status.SUCCESS, status) + assertEquals(1, data!!.size) + } + + assertEquals(3.5, items[1].data?.single { it.subject == "Język polski" }!!.average, .0) // from details and after set custom plus/minus + } + + @Test + fun `calc both semesters average with grade without grade in second semester`() { + every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + + coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } + coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flowWithResource { + listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)) to listOf(getSummary(semesters[2].semesterId, "Język polski", 2.5)) + } + + val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage(student, semesters[2].semesterId, false).getResult() } + + assertEquals(3.5, items.single { it.subject == "Język polski" }.average, .0) + } + @Test fun `force calc average on no grades`() { every { preferencesRepository.gradeAverageForceCalc } returns true