Skip to content

Commit 03d6357

Browse files
siddh1004Siddharth Agarwalmsasikanth
authored
Add effect to calculate lab based cvd risk (#5258)
https://app.shortcut.com/simpledotorg/story/14630/calculate-lab-based-cvd-risk-score --------- Co-authored-by: Siddharth Agarwal <[email protected]> Co-authored-by: Sasikanth Miriyampalli <[email protected]>
1 parent 5a41eee commit 03d6357

File tree

15 files changed

+587
-150
lines changed

15 files changed

+587
-150
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
- Add `cholesterol_value` to `MedicalHistory` table
6565
- Bump AGP to v8.8.1
6666
- Fix JSON variable name in non-lab based statin calculation sheet
67+
- Add `LabBasedCVDRiskCalculator` and effect to calculate lab based cvd risk.
6768

6869
### Fixes
6970

app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskCalculationSheet.kt

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,54 @@ package org.simple.clinic.cvdrisk
22

33
import com.squareup.moshi.Json
44
import com.squareup.moshi.JsonClass
5+
import org.simple.clinic.medicalhistory.Answer
56

67
@JsonClass(generateAdapter = true)
78
data class LabBasedCVDRiskCalculationSheet(
89
val diabetes: DiabetesRisk,
9-
@Json(name = "nodiabetes") val noDiabetes: NoDiabetesRisk,
10+
@Json(name = "nodiabetes") val noDiabetes: DiabetesRisk,
1011
) : CVDRiskCalculationSheet {
1112

1213
@JsonClass(generateAdapter = true)
1314
data class DiabetesRisk(
1415
val women: Women<LabBasedRiskEntry>,
1516
val men: Men<LabBasedRiskEntry>
1617
)
17-
18-
@JsonClass(generateAdapter = true)
19-
data class NoDiabetesRisk(
20-
val women: Women<LabBasedRiskEntry>,
21-
val men: Men<LabBasedRiskEntry>
22-
)
2318
}
2419

2520
@JsonClass(generateAdapter = true)
2621
data class NonLabBasedCVDRiskCalculationSheet(
2722
val women: Women<NonLabBasedRiskEntry>,
2823
val men: Men<NonLabBasedRiskEntry>
29-
): CVDRiskCalculationSheet
24+
) : CVDRiskCalculationSheet
3025

3126
sealed interface CVDRiskCalculationSheet
3227

3328
@JsonClass(generateAdapter = true)
34-
data class Women<T: RiskEntry>(
29+
data class Women<T : RiskEntry>(
3530
override val smoking: AgeData<T>,
3631
@Json(name = "nonsmoking")
3732
override val nonSmoking: AgeData<T>
38-
): SmokingData<T>
33+
) : SmokingData<T>
3934

4035
@JsonClass(generateAdapter = true)
41-
data class Men<T: RiskEntry>(
36+
data class Men<T : RiskEntry>(
4237
override val smoking: AgeData<T>,
4338
@Json(name = "nonsmoking")
4439
override val nonSmoking: AgeData<T>
45-
): SmokingData<T>
40+
) : SmokingData<T>
4641

47-
sealed interface SmokingData<T: RiskEntry> {
42+
sealed interface SmokingData<T : RiskEntry> {
4843
val smoking: AgeData<T>
4944
val nonSmoking: AgeData<T>
45+
46+
fun ageDataForSmokingStatus(isSmoker: Answer): List<AgeData<T>> {
47+
return when (isSmoker) {
48+
Answer.Yes -> listOf(smoking)
49+
Answer.No -> listOf(nonSmoking)
50+
else -> listOf(nonSmoking, smoking)
51+
}
52+
}
5053
}
5154

5255
@JsonClass(generateAdapter = true)
@@ -58,20 +61,47 @@ data class AgeData<T : RiskEntry>(
5861
@Json(name = "60 - 64") val age60to64: List<T>,
5962
@Json(name = "65 - 69") val age65to69: List<T>,
6063
@Json(name = "70 - 74") val age70to74: List<T>
61-
)
64+
) {
65+
66+
fun riskForAge(age: Int): List<T> {
67+
return when (age) {
68+
in 40..44 -> age40to44
69+
in 45..49 -> age45to49
70+
in 50..54 -> age50to54
71+
in 55..59 -> age55to59
72+
in 60..64 -> age60to64
73+
in 65..69 -> age65to69
74+
in 70..74 -> age70to74
75+
else -> emptyList()
76+
}
77+
}
78+
}
6279

6380
@JsonClass(generateAdapter = true)
6481
data class LabBasedRiskEntry(
65-
@Json(name = "sbp") val systolic: String,
82+
@Json(name = "sbp") override val systolic: String,
6683
@Json(name = "chol") val cholesterol: String,
6784
val risk: Int
6885
) : RiskEntry
6986

7087
@JsonClass(generateAdapter = true)
7188
data class NonLabBasedRiskEntry(
72-
@Json(name = "sbp") val systolic: String,
89+
@Json(name = "sbp") override val systolic: String,
7390
val bmi: String,
7491
val risk: Int
7592
) : RiskEntry
7693

77-
sealed interface RiskEntry
94+
sealed interface RiskEntry {
95+
96+
val systolic: String
97+
98+
fun isInSystolicRange(systolic: Int): Boolean {
99+
val systolicRange = when (systolic) {
100+
in 0..159 -> "140 - 159"
101+
in 160..179 -> "160 - 179"
102+
else -> ">= 180"
103+
}
104+
105+
return this.systolic == systolicRange
106+
}
107+
}

app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskCalculator.kt

Lines changed: 0 additions & 91 deletions
This file was deleted.

app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskRange.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ data class CVDRiskRange(
3737
}
3838

3939
companion object {
40+
41+
fun from(risks: List<Int>): CVDRiskRange? {
42+
return when {
43+
risks.isEmpty() -> null
44+
risks.size == 1 -> CVDRiskRange(min = risks.first(), max = risks.first())
45+
else -> CVDRiskRange(risks.min(), risks.max())
46+
}
47+
}
48+
4049
fun parseRiskRange(risk: String): CVDRiskRange {
4150
val risks = risk.split("-").map { it.trim() }
4251
return when (risks.size) {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.simple.clinic.cvdrisk
2+
3+
import org.simple.clinic.medicalhistory.Answer
4+
import org.simple.clinic.patient.Gender
5+
6+
data class LabBasedCVDRiskInput(
7+
val gender: Gender,
8+
val age: Int,
9+
val systolic: Int,
10+
val isSmoker: Answer = Answer.Unanswered,
11+
val diagnosedWithDiabetes: Answer = Answer.Unanswered,
12+
val cholesterol: Float? = null,
13+
)

app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskInput.kt renamed to app/src/main/java/org/simple/clinic/cvdrisk/NonLabBasedCVDRiskInput.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package org.simple.clinic.cvdrisk
33
import org.simple.clinic.medicalhistory.Answer
44
import org.simple.clinic.patient.Gender
55

6-
data class CVDRiskInput(
6+
data class NonLabBasedCVDRiskInput(
77
val gender: Gender,
88
val age: Int,
99
val systolic: Int,
1010
val isSmoker: Answer = Answer.Unanswered,
11-
val bmi: Float? = null
11+
val bmi: Float? = null,
1212
)
13+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.simple.clinic.cvdrisk.calculator
2+
3+
import dagger.Lazy
4+
import org.simple.clinic.cvdrisk.CVDRiskRange
5+
import org.simple.clinic.cvdrisk.LabBasedCVDRiskCalculationSheet
6+
import org.simple.clinic.cvdrisk.LabBasedCVDRiskInput
7+
import org.simple.clinic.cvdrisk.LabBasedRiskEntry
8+
import org.simple.clinic.di.AppScope
9+
import org.simple.clinic.medicalhistory.Answer
10+
import org.simple.clinic.patient.Gender
11+
import javax.inject.Inject
12+
13+
@AppScope
14+
class LabBasedCVDRiskCalculator @Inject constructor(
15+
private val labBasedCVDRiskCalculationSheet: Lazy<LabBasedCVDRiskCalculationSheet?>,
16+
) {
17+
18+
fun calculateCvdRisk(cvdRiskInput: LabBasedCVDRiskInput): CVDRiskRange? {
19+
with(cvdRiskInput) {
20+
val riskEntries = getLabBasedRiskEntries(cvdRiskInput) ?: return null
21+
val cholesterolRangeList = getCholesterolRangeList(cholesterol)
22+
val risks =
23+
riskEntries
24+
.filter { it.isInSystolicRange(systolic) && it.cholesterol in cholesterolRangeList }
25+
.map { it.risk }
26+
27+
return CVDRiskRange.from(risks)
28+
}
29+
}
30+
31+
private fun getLabBasedRiskEntries(cvdRiskInput: LabBasedCVDRiskInput): List<LabBasedRiskEntry>? {
32+
with(cvdRiskInput) {
33+
val sheet = labBasedCVDRiskCalculationSheet.get()
34+
val diabetesData = sheet?.let { getDiabetesData(it, diagnosedWithDiabetes) }
35+
val genderData = diabetesData?.let { getGenderData(it, gender) }
36+
val smokingDataList = genderData?.ageDataForSmokingStatus(isSmoker)
37+
38+
return smokingDataList?.flatMap { it.riskForAge(age) }
39+
}
40+
}
41+
42+
private fun getGenderData(genderData: LabBasedCVDRiskCalculationSheet.DiabetesRisk, gender: Gender) = when (gender) {
43+
Gender.Female -> genderData.women
44+
Gender.Male -> genderData.men
45+
else -> null
46+
}
47+
48+
private fun getDiabetesData(diabetesData: LabBasedCVDRiskCalculationSheet, answer: Answer) =
49+
when (answer) {
50+
Answer.Yes -> diabetesData.diabetes
51+
Answer.No -> diabetesData.noDiabetes
52+
else -> null
53+
}
54+
55+
56+
private fun getCholesterolRangeList(cholesterol: Float?): List<String> {
57+
return cholesterol?.let { listOf(getCholesterolRange(it)) }
58+
?: listOf("< 4", "4 - 4.9", "5 - 5.9", "6 - 6.9", ">= 7")
59+
}
60+
61+
private fun getCholesterolRange(cholesterol: Float): String {
62+
return when {
63+
cholesterol >= 0.0 && cholesterol < 4.0 -> "< 4"
64+
cholesterol >= 4.0 && cholesterol < 5.0 -> "4 - 4.9"
65+
cholesterol >= 5.0 && cholesterol < 6.0 -> "5 - 5.9"
66+
cholesterol >= 6.0 && cholesterol < 7.0 -> "6 - 6.9"
67+
else -> ">= 7"
68+
}
69+
}
70+
71+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.simple.clinic.cvdrisk.calculator
2+
3+
import dagger.Lazy
4+
import org.simple.clinic.cvdrisk.CVDRiskRange
5+
import org.simple.clinic.cvdrisk.NonLabBasedCVDRiskCalculationSheet
6+
import org.simple.clinic.cvdrisk.NonLabBasedCVDRiskInput
7+
import org.simple.clinic.cvdrisk.NonLabBasedRiskEntry
8+
import org.simple.clinic.di.AppScope
9+
import org.simple.clinic.patient.Gender
10+
import javax.inject.Inject
11+
12+
@AppScope
13+
class NonLabBasedCVDRiskCalculator @Inject constructor(
14+
private val nonLabBasedCVDRiskCalculationSheet: Lazy<NonLabBasedCVDRiskCalculationSheet?>,
15+
) {
16+
fun calculateCvdRisk(cvdRiskInput: NonLabBasedCVDRiskInput): CVDRiskRange? {
17+
with(cvdRiskInput) {
18+
val riskEntries = getNonLabBasedRiskEntries(cvdRiskInput) ?: return null
19+
val bmiRangeList = getBMIRangeList(bmi)
20+
val risks = riskEntries
21+
.filter { it.isInSystolicRange(systolic) && it.bmi in bmiRangeList }
22+
.map { it.risk }
23+
24+
return CVDRiskRange.from(risks)
25+
}
26+
}
27+
28+
private fun getNonLabBasedRiskEntries(cvdRiskInput: NonLabBasedCVDRiskInput): List<NonLabBasedRiskEntry>? {
29+
with(cvdRiskInput) {
30+
val sheet = nonLabBasedCVDRiskCalculationSheet.get()
31+
val genderData = sheet?.let { getGenderData(it, gender) }
32+
val smokingDataList = genderData?.ageDataForSmokingStatus(isSmoker)
33+
34+
return smokingDataList?.flatMap { it.riskForAge(age) }
35+
}
36+
}
37+
38+
private fun getGenderData(genderData: NonLabBasedCVDRiskCalculationSheet, gender: Gender) = when (gender) {
39+
Gender.Female -> genderData.women
40+
Gender.Male -> genderData.men
41+
else -> null
42+
}
43+
44+
private fun getBMIRangeList(bmi: Float?): List<String> {
45+
return bmi?.let { listOf(getBMIRange(it)) }
46+
?: listOf("< 20", "20 - 24", "25 - 29", "30 - 35", "> 35")
47+
}
48+
49+
private fun getBMIRange(bmi: Float): String {
50+
return when (bmi) {
51+
in 0.0..19.9 -> "< 20"
52+
in 20.0..24.9 -> "20 - 24"
53+
in 25.0..29.9 -> "25 - 29"
54+
in 30.0..34.9 -> "30 - 35"
55+
else -> "> 35"
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)