Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,34 @@ internal fun StringType.toIdType(): IdType {
return IdType(value)
}

/**
* Checks if two Coding objects match.
*
* The matching logic is progressive:
* 1. Always matches on the [code].
* 2. Matches on [system] if the both coding has a system.
* 3. Matches on [version] if the both coding has a version.
*/
internal fun Coding.matches(other: Coding): Boolean {
// Always match on code
if (this.code != other.code) {
return false
}

// If system exists in both, it must match
if (other.hasSystem() && this.hasSystem() && this.system != other.system) {
return false
}

// If version exists in both, it must match
if (other.hasVersion() && this.hasVersion() && this.version != other.version) {
return false
}

// All conditions met
return true
}

internal fun StringType.localizedTextAnnotatedString(): AnnotatedString? {
return this.getLocalizedText()?.toAnnotatedString()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package com.google.android.fhir.datacapture.mapping

import com.google.android.fhir.datacapture.extensions.copyNestedItemsToChildlessAnswers
import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem
import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension
import com.google.android.fhir.datacapture.extensions.initialExpression
import com.google.android.fhir.datacapture.extensions.initialSelected
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.datacapture.extensions.matches
import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts
import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnderAnswers
import com.google.android.fhir.datacapture.extensions.targetStructureMap
import com.google.android.fhir.datacapture.extensions.toCodeType
import com.google.android.fhir.datacapture.extensions.toCoding
Expand Down Expand Up @@ -228,21 +230,30 @@ object ResourceMapper {
launchContexts,
questionnaire.questionnaireLaunchContexts ?: listOf(),
)
populateInitialValues(questionnaire.item, filteredLaunchContexts)
return QuestionnaireResponse().apply {
item = questionnaire.item.map { it.createQuestionnaireResponseItem() }
item =
createPopulatedResponseItems(questionnaire.item, filteredLaunchContexts).toMutableList()
}
}

private suspend fun populateInitialValues(
private suspend fun createPopulatedResponseItems(
questionnaireItems: List<Questionnaire.QuestionnaireItemComponent>,
launchContexts: Map<String, Resource>,
) {
questionnaireItems.forEach { populateInitialValue(it, launchContexts) }
): List<QuestionnaireResponse.QuestionnaireResponseItemComponent> {
return questionnaireItems.map { it.createPopulatedResponseItem(launchContexts) }
}

private suspend fun populateInitialValue(
private suspend fun Questionnaire.QuestionnaireItemComponent.createPopulatedResponseItem(
launchContexts: Map<String, Resource>,
): QuestionnaireResponse.QuestionnaireResponseItemComponent {
val questionnaireResponseItem = createQuestionnaireResponseItem()
populateQuestionnaireResponseItem(this, questionnaireResponseItem, launchContexts)
return questionnaireResponseItem
}

private suspend fun populateQuestionnaireResponseItem(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
launchContexts: Map<String, Resource>,
) {
check(questionnaireItem.initial.isEmpty() || questionnaireItem.initialExpression == null) {
Expand Down Expand Up @@ -288,23 +299,61 @@ object ResourceMapper {
* as additional options, nor would it make sense to do so. This behavior ensures the answer
* options remain consistent with the defined set.
*/
if (questionnaireItem.answerOption.isNotEmpty()) {
questionnaireItem.answerOption.forEach { answerOption ->
answerOption.initialSelected =
evaluatedExpressionResult.any { answerOption.value.equalsDeep(it) }
}
} else {
questionnaireItem.initial =
val answers =
if (questionnaireItem.answerOption.isEmpty()) {
evaluatedExpressionResult.map {
Questionnaire.QuestionnaireItemInitialComponent()
.setValue(
it,
)
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = it }
}
} else {
questionnaireItem.answerOption.mapNotNull { answerOption ->
val optionValue = answerOption.value ?: return@mapNotNull null
if (
evaluatedExpressionResult.any { evaluatedValue ->
if (optionValue is Coding && evaluatedValue is Coding) {
optionValue.matches(evaluatedValue)
} else {
optionValue.equalsDeep(evaluatedValue)
}
}
) {
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = optionValue
}
} else {
null
}
}
}
questionnaireResponseItem.answer = answers.toMutableList()
if (
questionnaireItem.shouldHaveNestedItemsUnderAnswers &&
questionnaireResponseItem.answer.isNotEmpty()
) {
questionnaireResponseItem.copyNestedItemsToChildlessAnswers(questionnaireItem)
}
}

populateInitialValues(questionnaireItem.item, launchContexts)
if (questionnaireItem.shouldHaveNestedItemsUnderAnswers) {
questionnaireResponseItem.answer.orEmpty().forEach { answerComponent ->
questionnaireItem.item.zipByLinkId(answerComponent.item.orEmpty()) {
childQuestionnaireItem,
childResponseItem,
->
populateQuestionnaireResponseItem(
childQuestionnaireItem,
childResponseItem,
launchContexts,
)
}
}
} else {
questionnaireItem.item.zipByLinkId(questionnaireResponseItem.item.orEmpty()) {
childQuestionnaireItem,
childResponseItem,
->
populateQuestionnaireResponseItem(childQuestionnaireItem, childResponseItem, launchContexts)
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 Google LLC
* Copyright 2022-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -280,4 +280,92 @@ class MoreTypesTest {
val quantity = Quantity(20L)
assertThat(quantity.getValueAsString(context)).isEqualTo("20")
}

@Test
fun codingMatches_sameSystemAndCode_shouldReturnTrue() {
val left = Coding("system", "code", "display")
val right = Coding("system", "code", "display")

assertThat(left.matches(right)).isTrue()
}

@Test
fun codingMatches_differentDisplays_shouldReturnTrue() {
val left = Coding("system", "code", "display")
val right = Coding("system", "code", "other display")

assertThat(left.matches(right)).isTrue()
}

@Test
fun codingMatches_differentCodes_shouldReturnFalse() {
val left = Coding("system", "code", "display")
val right = Coding("system", "other-code", "display")

assertThat(left.matches(right)).isFalse()
}

@Test
fun codingMatches_differentSystems_shouldReturnFalse() {
val left = Coding("system", "code", "display")
val right = Coding("other-system", "code", "display")

assertThat(left.matches(right)).isFalse()
}

@Test
fun codingMatches_sameVersion_shouldReturnTrue() {
val left = Coding("system", "code", "display").apply { version = "1" }
val right = Coding("system", "code", "display").apply { version = "1" }

assertThat(left.matches(right)).isTrue()
}

@Test
fun codingMatches_differentVersions_shouldReturnFalse() {
val left = Coding("system", "code", "display").apply { version = "1" }
val right = Coding("system", "code", "display").apply { version = "2" }

assertThat(left.matches(right)).isFalse()
}

@Test
fun codingMatches_missingSystemOnOneSide_shouldReturnTrue() {
val left = Coding(null, "code", "display")
val right = Coding("system", "code", "display")

assertThat(left.matches(right)).isTrue()
}

@Test
fun codingMatches_missingVersionOnOneSide_shouldReturnTrue() {
val left = Coding("system", "code", "display").apply { version = "1" }
val right = Coding("system", "code", "display")

assertThat(left.matches(right)).isTrue()
}

@Test
fun codingMatches_missingDisplayOnOneSide_shouldReturnTrue() {
val left = Coding("system", "code", null)
val right = Coding("system", "code", "display")

assertThat(left.matches(right)).isTrue()
}

@Test
fun codingMatches_bothMissingSystem_shouldReturnTrue() {
val left = Coding(null, "code", "display")
val right = Coding(null, "code", "display")

assertThat(left.matches(right)).isTrue()
}

@Test
fun codingMatches_bothMissingDisplay_shouldReturnTrue() {
val left = Coding("system", "code", null)
val right = Coding("system", "code", null)

assertThat(left.matches(right)).isTrue()
}
}
Loading
Loading