1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-02-20 20:54: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
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<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) {
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
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<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
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.GradeSummary
data class GradeDetailsWithAverage(
data class GradeSubject(
val subject: String,
val average: Double,
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.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<GradeDetailsWithAverage>) {
newGradesAmount = grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } }
private fun updateNewGradesAmount(grades: List<GradeSubject>) {
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<GradeDetailsWithAverage>): List<GradeDetailsItem> {
private fun createGradeItems(items: List<GradeSubject>): List<GradeDetailsItem> {
return items
.let { gradesWithAverages ->
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.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<GradeDetailsWithAverage>): List<GradeSummary> {
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
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()

View File

@ -16,63 +16,58 @@ fun List<Grade>.calcAverage(): Double {
}
@JvmName("calcSummaryAverage")
fun List<GradeSummary>.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<GradeSummary>.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
}

View File

@ -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