1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-02-21 19:24:44 +01:00

Fix null data in Status.SUCCESS in emission from average provider (#1105)

This commit is contained in:
Rafał Borcz 2021-01-30 16:07:37 +01:00 committed by GitHub
parent dd5ce752da
commit d1cd497a23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 250 additions and 126 deletions

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.grade package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.Resource 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.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
@ -33,81 +34,162 @@ class GradeAverageProvider @Inject constructor(
private val minusModifier get() = preferencesRepository.gradeMinusModifier private val minusModifier get() = preferencesRepository.gradeMinusModifier
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = flowWithResourceIn { fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) =
val semesters = semesterRepository.getSemesters(student) flowWithResourceIn {
val semesters = semesterRepository.getSemesters(student)
when (preferencesRepository.gradeAverageMode) { when (preferencesRepository.gradeAverageMode) {
ONE_SEMESTER -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh) ONE_SEMESTER -> getGradeSubjects(
BOTH_SEMESTERS -> calculateBothSemestersAverage(student, semesters, semesterId, forceRefresh) student = student,
ALL_YEAR -> calculateAllYearAverage(student, semesters, semesterId, forceRefresh) semester = semesters.single { it.semesterId == semesterId },
} forceRefresh = forceRefresh
}.distinctUntilChanged() )
BOTH_SEMESTERS -> calculateCombinedAverage(
private fun calculateBothSemestersAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> { student = student,
val selectedSemester = semesters.single { it.semesterId == semesterId } semesters = semesters,
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } semesterId = semesterId,
forceRefresh = forceRefresh,
val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh) averageMode = BOTH_SEMESTERS
)
return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else { ALL_YEAR -> calculateCombinedAverage(
val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh) student = student,
selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails -> semesters = semesters,
val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 } semesterId = semesterId,
secondDetails.copy(data = selectedDetails.data?.map { selected -> forceRefresh = forceRefresh,
val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject } averageMode = ALL_YEAR
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<Semester>, semesterId: Int, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> {
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<Resource<List<GradeDetailsWithAverage>>> {
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
) )
} }
}.distinctUntilChanged()
Resource(res.status, items, res.error) private fun calculateCombinedAverage(
student: Student,
semesters: List<Semester>,
semesterId: Int,
forceRefresh: Boolean,
averageMode: GradeAverageMode
): Flow<Resource<List<GradeSubject>>> {
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<GradeSummary>.emulateEmptySummaries(student: Student, semester: Semester, grades: List<Pair<String, List<Grade>>>, calcAverage: Boolean): List<GradeSummary> { 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<Resource<List<GradeSubject>>> {
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<GradeSummary>.emulateEmptySummaries(
student: Student,
semester: Semester,
grades: List<Pair<String, List<Grade>>>,
calcAverage: Boolean
): List<GradeSummary> {
if (isNotEmpty() && size > grades.size) return this if (isNotEmpty() && size > grades.size) return this
return grades.mapIndexed { i, (subject, details) -> return grades.mapIndexed { i, (subject, details) ->

View File

@ -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.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
data class GradeDetailsWithAverage( data class GradeSubject(
val subject: String, val subject: String,
val average: Double, val average: Double,
val points: String, val points: String,

View File

@ -10,9 +10,9 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider 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.ALPHABETIC
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE 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.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
@ -201,8 +201,9 @@ class GradeDetailsPresenter @Inject constructor(
}.launch() }.launch()
} }
private fun updateNewGradesAmount(grades: List<GradeDetailsWithAverage>) { private fun updateNewGradesAmount(grades: List<GradeSubject>) {
newGradesAmount = grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } } newGradesAmount =
grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } }
} }
private fun showErrorViewOnError(message: String, error: Throwable) { private fun showErrorViewOnError(message: String, error: Throwable) {
@ -217,7 +218,7 @@ class GradeDetailsPresenter @Inject constructor(
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun createGradeItems(items: List<GradeDetailsWithAverage>): List<GradeDetailsItem> { private fun createGradeItems(items: List<GradeSubject>): List<GradeDetailsItem> {
return items return items
.let { gradesWithAverages -> .let { gradesWithAverages ->
if (!preferencesRepository.showSubjectsWithoutGrades) { if (!preferencesRepository.showSubjectsWithoutGrades) {

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider 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.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.flowWithResourceIn
@ -135,14 +135,14 @@ class GradeSummaryPresenter @Inject constructor(
cancelJobs("load") cancelJobs("load")
} }
private fun createGradeSummaryItems(items: List<GradeDetailsWithAverage>): List<GradeSummary> { private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
return items return items
.filter { !checkEmpty(it) } .filter { !checkEmpty(it) }
.sortedBy { it.subject } .sortedBy { it.subject }
.map { it.summary.copy(average = it.average) } .map { it.summary.copy(average = it.average) }
} }
private fun checkEmpty(gradeSummary: GradeDetailsWithAverage): Boolean { private fun checkEmpty(gradeSummary: GradeSubject): Boolean {
return gradeSummary.run { return gradeSummary.run {
summary.finalGrade.isBlank() summary.finalGrade.isBlank()
&& summary.predictedGrade.isBlank() && summary.predictedGrade.isBlank()

View File

@ -16,63 +16,58 @@ fun List<Grade>.calcAverage(): Double {
} }
@JvmName("calcSummaryAverage") @JvmName("calcSummaryAverage")
fun List<GradeSummary>.calcAverage(): Double { fun List<GradeSummary>.calcAverage() = asSequence()
return asSequence().mapNotNull { .mapNotNull {
if (it.finalGrade.matches("[0-6]".toRegex())) it.finalGrade.toDouble() else null if (it.finalGrade.matches("[0-6]".toRegex())) {
}.average().let { if (it.isNaN()) 0.0 else it } it.finalGrade.toDouble()
} } else null
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
}
} }
} .average()
.let { if (it.isNaN()) 0.0 else it }
fun Grade.getGradeColor(): Int { fun Grade.getBackgroundColor(theme: String) = when (theme) {
return when (color) { "grade_color" -> getGradeColor()
"000000" -> R.color.grade_black "material" -> when (value.toInt()) {
"F04C4C" -> R.color.grade_red 6 -> R.color.grade_material_six
"20A4F7" -> R.color.grade_blue 5 -> R.color.grade_material_five
"6ECD07" -> R.color.grade_green 4 -> R.color.grade_material_four
"B16CF1" -> R.color.grade_purple 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 -> 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 inline val Grade.colorStringId: Int
get() { get() = when (color) {
return when (color) { "000000" -> R.string.all_black
"000000" -> R.string.all_black "F04C4C" -> R.string.all_red
"F04C4C" -> R.string.all_red "20A4F7" -> R.string.all_blue
"20A4F7" -> R.string.all_blue "6ECD07" -> R.string.all_green
"6ECD07" -> R.string.all_green "B16CF1" -> R.string.all_purple
"B16CF1" -> R.string.all_purple else -> R.string.all_empty_color
else -> R.string.all_empty_color
}
} }
fun Grade.changeModifier(plusModifier: Double, minusModifier: Double): Grade { fun Grade.changeModifier(plusModifier: Double, minusModifier: Double) = when {
return when { modifier > 0 -> copy(modifier = plusModifier)
modifier > 0 -> copy(modifier = plusModifier) modifier < 0 -> copy(modifier = -minusModifier)
modifier < 0 -> copy(modifier = -minusModifier) else -> this
else -> this
}
} }

View File

@ -15,6 +15,7 @@ import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList 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 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 @Test
fun `force calc average on no grades`() { fun `force calc average on no grades`() {
every { preferencesRepository.gradeAverageForceCalc } returns true every { preferencesRepository.gradeAverageForceCalc } returns true