Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor kpi calculation #7

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,9 @@ internal abstract class BaseKpiCalculationStrategy : KpiCalculationStrategy {
return KpiCalculationResult.Empty()
}

val incompleteResults = if (!strict) {
childScores.mapNotNull {
val res = it.first
if (res is KpiCalculationResult.Incomplete) {
Pair(KpiCalculationResult.Success(score = res.score), it.second)
} else {
null
}
}
} else {
emptyList()
}

@Suppress("UNCHECKED_CAST")
val successScores = listOf(
childScores.filter { it.first is KpiCalculationResult.Success },
incompleteResults
).flatten() as List<Pair<KpiCalculationResult.Success, Double>>

val failed = childScores
.filter {
it.first is KpiCalculationResult.Error
|| (it.first is KpiCalculationResult.Empty)
}

val missingEdgeWeights = failed.sumOf { it.second }
val (successScores, failed, missingEdgeWeights, hasIncompleteResults) = processChildScores(childScores, strict)

// distributes weights of incomplete edges evenly between existing edges
val additionalWeight =
if (missingEdgeWeights == 0.0)
0.0
Expand All @@ -97,9 +73,85 @@ internal abstract class BaseKpiCalculationStrategy : KpiCalculationStrategy {
successScores = successScores,
failed = failed,
additionalWeight = additionalWeight,
hasIncompleteResults = incompleteResults.isNotEmpty() || failed.isNotEmpty()
hasIncompleteResults = hasIncompleteResults
)
}

private data class SeparatedKpiResults(
val successScores: List<Pair<KpiCalculationResult.Success, Double>>,
val failedScores: List<Pair<KpiCalculationResult, Double>>,
val missingEdgeWeights: Double,
val hasIncompleteResults: Boolean
)

/**
* Sorts the given childScores into separate lists for Success and Failed results.
* The sorting of Incomplete results depends on the strict mode.
*
* In strict mode, we consider Incomplete results as failed, and they are not
* considered in the KPI calculation.
* In not strict mode, we cast Incomplete results to Success results and use
* them for further calculation.
*
* @param childScores the KPI results and their planned edge weights.
* @param strict mode which influences the separation.
*
* @return SeparatedKpiResults, which contain a List of success and failed scores,
* missingEdgeWeights (the sum of all failed node's edge weights),
* hasIncompleteResults (indicates whether the original childScores contained an
* Incomplete result)
*/
private fun processChildScores(
childScores: Collection<Pair<KpiCalculationResult, Double>>,
strict: Boolean
): SeparatedKpiResults {

val successScores = mutableListOf<Pair<KpiCalculationResult.Success, Double>>()
val failedScores = mutableListOf<Pair<KpiCalculationResult, Double>>()
var missingEdgeWeight = 0.0
var hasIncompleteResults = false

childScores.forEach { childScore ->

// we need to extract the pair into a single variable to enable
// smart type casting in the following
when (val calcResult = childScore.first) {
is KpiCalculationResult.Success -> {
// type erasure can't handle parameterized types so smart
// casting won't work
@Suppress("UNCHECKED_CAST")
successScores.add(childScore as Pair<KpiCalculationResult.Success, Double>)
}

is KpiCalculationResult.Incomplete -> {
if (strict) {
failedScores.add(childScore)
missingEdgeWeight += childScore.second
} else {
successScores.add(
Pair(
KpiCalculationResult.Success(score = calcResult.score),
childScore.second
)
)
}
hasIncompleteResults = true
}

is KpiCalculationResult.Empty, is KpiCalculationResult.Error -> {
failedScores.add(childScore)
missingEdgeWeight += childScore.second
hasIncompleteResults = true
}
}
}

return SeparatedKpiResults(
successScores = successScores,
failedScores = failedScores,
missingEdgeWeights = missingEdgeWeight,
hasIncompleteResults = hasIncompleteResults
)
}

protected abstract fun internalCalculateKpi(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package de.fraunhofer.iem.kpiCalculator.core.strategy

import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.fail

class AbstractKpiCalculationTest {

@Test
fun calculateKpiEmptyChildren() {
fun callback(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
): Unit {
fail()
}

val testStrategy = TestStrategy(callback = ::callback)

val emptyRelaxed = testStrategy.calculateKpi(
childScores = emptyList(),
strict = false
)

assert(emptyRelaxed is KpiCalculationResult.Empty)

val emptyStrict = testStrategy.calculateKpi(
childScores = emptyList(),
strict = true
)

assert(emptyStrict is KpiCalculationResult.Empty)
}

@Test
fun calculateKpiSuccess() {

val childScores: List<Pair<KpiCalculationResult, Double>> = listOf(
Pair(KpiCalculationResult.Success(score = 10), 0.4),
Pair(KpiCalculationResult.Success(score = 10), 0.4),
Pair(KpiCalculationResult.Success(score = 10), 0.4),
)

fun callback(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) {
assertEquals(successScores.size, childScores.size)
assert(failed.isEmpty())
assertEquals(additionalWeight, 0.0)
assert(!hasIncompleteResults)
}

val testStrategy = TestStrategy(callback = ::callback)


testStrategy.calculateKpi(childScores = childScores, strict = false)
testStrategy.calculateKpi(childScores = childScores, strict = true)
}

@Test
fun calculateKpiIncomplete() {

val childScores: List<Pair<KpiCalculationResult, Double>> = listOf(
Pair(KpiCalculationResult.Incomplete(score = 10, reason = "", additionalWeights = 0.2), 0.4),
Pair(KpiCalculationResult.Incomplete(score = 10, reason = "", additionalWeights = 0.2), 0.4),
Pair(KpiCalculationResult.Incomplete(score = 10, reason = "", additionalWeights = 0.2), 0.2),
)

fun callbackRelaxed(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) {
assertEquals(successScores.size, childScores.size)
assert(failed.isEmpty())
assertEquals(additionalWeight, 0.0)
assert(hasIncompleteResults)
}

val testStrategy = TestStrategy(callback = ::callbackRelaxed)
testStrategy.calculateKpi(childScores = childScores, strict = false)

fun callbackStrict(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) {
assert(successScores.isEmpty())
assert(failed.isEmpty())
assertEquals(additionalWeight, 0.0)
assert(hasIncompleteResults)
}

val testStrategyStrict = TestStrategy(callback = ::callbackStrict)
testStrategyStrict.calculateKpi(childScores = childScores, strict = true)
}

@Test
fun calculateKpiError() {

val childScores: List<Pair<KpiCalculationResult, Double>> = listOf(
Pair(KpiCalculationResult.Empty(), 0.4),
Pair(KpiCalculationResult.Empty(), 0.4),
Pair(KpiCalculationResult.Error(reason = ""), 0.2),
)

fun callback(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) {
assert(successScores.isEmpty())
assertEquals(failed.size, childScores.size)
assertEquals(additionalWeight, 1.0)
assert(hasIncompleteResults)
}

val testStrategy = TestStrategy(callback = ::callback)
testStrategy.calculateKpi(childScores = childScores, strict = false)
testStrategy.calculateKpi(childScores = childScores, strict = true)

val childScoresMixed: List<Pair<KpiCalculationResult, Double>> = listOf(
Pair(KpiCalculationResult.Empty(), 0.4),
Pair(KpiCalculationResult.Success(score = 9), 0.4),
Pair(KpiCalculationResult.Error(reason = ""), 0.2),
)

fun callbackMixed(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) {
assertEquals(1, successScores.size)
assertEquals(2, failed.size)
assertEquals((0.4 + 0.2) / 1, additionalWeight)
assert(hasIncompleteResults)
}

val testStrategyMixed = TestStrategy(callback = ::callbackMixed)
testStrategyMixed.calculateKpi(childScores = childScoresMixed, strict = false)
testStrategyMixed.calculateKpi(childScores = childScoresMixed, strict = true)
}

@Test
fun calculateKpiMixed() {

val childScores: List<Pair<KpiCalculationResult, Double>> = listOf(
Pair(KpiCalculationResult.Success(score = 4), 0.15),
Pair(KpiCalculationResult.Success(score = 4), 0.15),
Pair(KpiCalculationResult.Incomplete(score = 4, reason = "", additionalWeights = 0.1), 0.3),
Pair(KpiCalculationResult.Empty(), 0.2),
Pair(KpiCalculationResult.Error(reason = ""), 0.2),
)

fun callbackRelaxed(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) {
assertEquals(3, successScores.size)
assertEquals(2, failed.size)
assertEquals((0.4 / 3), additionalWeight)
assert(hasIncompleteResults)
}

val testStrategyStrict = TestStrategy(callback = ::callbackRelaxed)
testStrategyStrict.calculateKpi(childScores = childScores, strict = false)

fun callbackStrict(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) {
assertEquals(2, successScores.size)
assertEquals(3, failed.size)
assertEquals((0.3 + 0.2 + 0.2) / 2, additionalWeight)
assert(hasIncompleteResults)
}

val testStrategyMixed = TestStrategy(callback = ::callbackStrict)
testStrategyMixed.calculateKpi(childScores = childScores, strict = true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package de.fraunhofer.iem.kpiCalculator.core.strategy

import de.fraunhofer.iem.kpiCalculator.model.kpi.KpiStrategyId
import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiCalculationResult
import de.fraunhofer.iem.kpiCalculator.model.kpi.hierarchy.KpiNode

internal class TestStrategy(
val callback: (
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
) -> Unit
) : BaseKpiCalculationStrategy() {
override val kpiStrategyId: KpiStrategyId
get() = TODO("Not yet implemented")

override fun internalCalculateKpi(
successScores: List<Pair<KpiCalculationResult.Success, Double>>,
failed: List<Pair<KpiCalculationResult, Double>>,
additionalWeight: Double,
hasIncompleteResults: Boolean
): KpiCalculationResult {
callback(successScores, failed, additionalWeight, hasIncompleteResults)
return KpiCalculationResult.Empty()
}

override fun internalIsValid(node: KpiNode, strict: Boolean): Boolean {
TODO("Not yet implemented")
}
}