Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
- Add `cholesterol_value` to `MedicalHistory` table
- Bump AGP to v8.8.1
- Fix JSON variable name in non-lab based statin calculation sheet
- Add `LabBasedCVDRiskCalculator` and effect to calculate lab based cvd risk.

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,54 @@ package org.simple.clinic.cvdrisk

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.simple.clinic.medicalhistory.Answer

@JsonClass(generateAdapter = true)
data class LabBasedCVDRiskCalculationSheet(
val diabetes: DiabetesRisk,
@Json(name = "nodiabetes") val noDiabetes: NoDiabetesRisk,
@Json(name = "nodiabetes") val noDiabetes: DiabetesRisk,
) : CVDRiskCalculationSheet {

@JsonClass(generateAdapter = true)
data class DiabetesRisk(
val women: Women<LabBasedRiskEntry>,
val men: Men<LabBasedRiskEntry>
)

@JsonClass(generateAdapter = true)
data class NoDiabetesRisk(
val women: Women<LabBasedRiskEntry>,
val men: Men<LabBasedRiskEntry>
)
}

@JsonClass(generateAdapter = true)
data class NonLabBasedCVDRiskCalculationSheet(
val women: Women<NonLabBasedRiskEntry>,
val men: Men<NonLabBasedRiskEntry>
): CVDRiskCalculationSheet
) : CVDRiskCalculationSheet

sealed interface CVDRiskCalculationSheet

@JsonClass(generateAdapter = true)
data class Women<T: RiskEntry>(
data class Women<T : RiskEntry>(
override val smoking: AgeData<T>,
@Json(name = "nonsmoking")
override val nonSmoking: AgeData<T>
): SmokingData<T>
) : SmokingData<T>

@JsonClass(generateAdapter = true)
data class Men<T: RiskEntry>(
data class Men<T : RiskEntry>(
override val smoking: AgeData<T>,
@Json(name = "nonsmoking")
override val nonSmoking: AgeData<T>
): SmokingData<T>
) : SmokingData<T>

sealed interface SmokingData<T: RiskEntry> {
sealed interface SmokingData<T : RiskEntry> {
val smoking: AgeData<T>
val nonSmoking: AgeData<T>

fun ageDataForSmokingStatus(isSmoker: Answer): List<AgeData<T>> {
return when (isSmoker) {
Answer.Yes -> listOf(smoking)
Answer.No -> listOf(nonSmoking)
else -> listOf(nonSmoking, smoking)
}
}
}

@JsonClass(generateAdapter = true)
Expand All @@ -58,20 +61,47 @@ data class AgeData<T : RiskEntry>(
@Json(name = "60 - 64") val age60to64: List<T>,
@Json(name = "65 - 69") val age65to69: List<T>,
@Json(name = "70 - 74") val age70to74: List<T>
)
) {

fun riskForAge(age: Int): List<T> {
return when (age) {
in 40..44 -> age40to44
in 45..49 -> age45to49
in 50..54 -> age50to54
in 55..59 -> age55to59
in 60..64 -> age60to64
in 65..69 -> age65to69
in 70..74 -> age70to74
else -> emptyList()
}
}
}

@JsonClass(generateAdapter = true)
data class LabBasedRiskEntry(
@Json(name = "sbp") val systolic: String,
@Json(name = "sbp") override val systolic: String,
@Json(name = "chol") val cholesterol: String,
val risk: Int
) : RiskEntry

@JsonClass(generateAdapter = true)
data class NonLabBasedRiskEntry(
@Json(name = "sbp") val systolic: String,
@Json(name = "sbp") override val systolic: String,
val bmi: String,
val risk: Int
) : RiskEntry

sealed interface RiskEntry
sealed interface RiskEntry {

val systolic: String

fun isInSystolicRange(systolic: Int): Boolean {
val systolicRange = when (systolic) {
in 0..159 -> "140 - 159"
in 160..179 -> "160 - 179"
else -> ">= 180"
}

return this.systolic == systolicRange
}
}
91 changes: 0 additions & 91 deletions app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskCalculator.kt

This file was deleted.

9 changes: 9 additions & 0 deletions app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskRange.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ data class CVDRiskRange(
}

companion object {

fun from(risks: List<Int>): CVDRiskRange? {
return when {
risks.isEmpty() -> null
risks.size == 1 -> CVDRiskRange(min = risks.first(), max = risks.first())
else -> CVDRiskRange(risks.min(), risks.max())
}
}

fun parseRiskRange(risk: String): CVDRiskRange {
val risks = risk.split("-").map { it.trim() }
return when (risks.size) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.simple.clinic.cvdrisk

import org.simple.clinic.medicalhistory.Answer
import org.simple.clinic.patient.Gender

data class LabBasedCVDRiskInput(
val gender: Gender,
val age: Int,
val systolic: Int,
val isSmoker: Answer = Answer.Unanswered,
val diagnosedWithDiabetes: Answer = Answer.Unanswered,
val cholesterol: Float? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package org.simple.clinic.cvdrisk
import org.simple.clinic.medicalhistory.Answer
import org.simple.clinic.patient.Gender

data class CVDRiskInput(
data class NonLabBasedCVDRiskInput(
val gender: Gender,
val age: Int,
val systolic: Int,
val isSmoker: Answer = Answer.Unanswered,
val bmi: Float? = null
val bmi: Float? = null,
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.simple.clinic.cvdrisk.calculator

import dagger.Lazy
import org.simple.clinic.cvdrisk.CVDRiskRange
import org.simple.clinic.cvdrisk.LabBasedCVDRiskCalculationSheet
import org.simple.clinic.cvdrisk.LabBasedCVDRiskInput
import org.simple.clinic.cvdrisk.LabBasedRiskEntry
import org.simple.clinic.di.AppScope
import org.simple.clinic.medicalhistory.Answer
import org.simple.clinic.patient.Gender
import javax.inject.Inject

@AppScope
class LabBasedCVDRiskCalculator @Inject constructor(
private val labBasedCVDRiskCalculationSheet: Lazy<LabBasedCVDRiskCalculationSheet?>,
) {

fun calculateCvdRisk(cvdRiskInput: LabBasedCVDRiskInput): CVDRiskRange? {
with(cvdRiskInput) {
val riskEntries = getLabBasedRiskEntries(cvdRiskInput) ?: return null
val cholesterolRangeList = getCholesterolRangeList(cholesterol)
val risks =
riskEntries
.filter { it.isInSystolicRange(systolic) && it.cholesterol in cholesterolRangeList }
.map { it.risk }

return CVDRiskRange.from(risks)
}
}

private fun getLabBasedRiskEntries(cvdRiskInput: LabBasedCVDRiskInput): List<LabBasedRiskEntry>? {
with(cvdRiskInput) {
val sheet = labBasedCVDRiskCalculationSheet.get()
val diabetesData = sheet?.let { getDiabetesData(it, diagnosedWithDiabetes) }
val genderData = diabetesData?.let { getGenderData(it, gender) }
val smokingDataList = genderData?.ageDataForSmokingStatus(isSmoker)

return smokingDataList?.flatMap { it.riskForAge(age) }
}
}

private fun getGenderData(genderData: LabBasedCVDRiskCalculationSheet.DiabetesRisk, gender: Gender) = when (gender) {
Gender.Female -> genderData.women
Gender.Male -> genderData.men
else -> null
}

private fun getDiabetesData(diabetesData: LabBasedCVDRiskCalculationSheet, answer: Answer) =
when (answer) {
Answer.Yes -> diabetesData.diabetes
Answer.No -> diabetesData.noDiabetes
else -> null
}


private fun getCholesterolRangeList(cholesterol: Float?): List<String> {
return cholesterol?.let { listOf(getCholesterolRange(it)) }
?: listOf("< 4", "4 - 4.9", "5 - 5.9", "6 - 6.9", ">= 7")
}

private fun getCholesterolRange(cholesterol: Float): String {
return when {
cholesterol >= 0.0 && cholesterol < 4.0 -> "< 4"
cholesterol >= 4.0 && cholesterol < 5.0 -> "4 - 4.9"
cholesterol >= 5.0 && cholesterol < 6.0 -> "5 - 5.9"
cholesterol >= 6.0 && cholesterol < 7.0 -> "6 - 6.9"
else -> ">= 7"
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.simple.clinic.cvdrisk.calculator

import dagger.Lazy
import org.simple.clinic.cvdrisk.CVDRiskRange
import org.simple.clinic.cvdrisk.NonLabBasedCVDRiskCalculationSheet
import org.simple.clinic.cvdrisk.NonLabBasedCVDRiskInput
import org.simple.clinic.cvdrisk.NonLabBasedRiskEntry
import org.simple.clinic.di.AppScope
import org.simple.clinic.patient.Gender
import javax.inject.Inject

@AppScope
class NonLabBasedCVDRiskCalculator @Inject constructor(
private val nonLabBasedCVDRiskCalculationSheet: Lazy<NonLabBasedCVDRiskCalculationSheet?>,
) {
fun calculateCvdRisk(cvdRiskInput: NonLabBasedCVDRiskInput): CVDRiskRange? {
with(cvdRiskInput) {
val riskEntries = getNonLabBasedRiskEntries(cvdRiskInput) ?: return null
val bmiRangeList = getBMIRangeList(bmi)
val risks = riskEntries
.filter { it.isInSystolicRange(systolic) && it.bmi in bmiRangeList }
.map { it.risk }

return CVDRiskRange.from(risks)
}
}

private fun getNonLabBasedRiskEntries(cvdRiskInput: NonLabBasedCVDRiskInput): List<NonLabBasedRiskEntry>? {
with(cvdRiskInput) {
val sheet = nonLabBasedCVDRiskCalculationSheet.get()
val genderData = sheet?.let { getGenderData(it, gender) }
val smokingDataList = genderData?.ageDataForSmokingStatus(isSmoker)

return smokingDataList?.flatMap { it.riskForAge(age) }
}
}

private fun getGenderData(genderData: NonLabBasedCVDRiskCalculationSheet, gender: Gender) = when (gender) {
Gender.Female -> genderData.women
Gender.Male -> genderData.men
else -> null
}

private fun getBMIRangeList(bmi: Float?): List<String> {
return bmi?.let { listOf(getBMIRange(it)) }
?: listOf("< 20", "20 - 24", "25 - 29", "30 - 35", "> 35")
}

private fun getBMIRange(bmi: Float): String {
return when (bmi) {
in 0.0..19.9 -> "< 20"
in 20.0..24.9 -> "20 - 24"
in 25.0..29.9 -> "25 - 29"
in 30.0..34.9 -> "30 - 35"
else -> "> 35"
}
}
}
Loading
Loading