Skip to content

Commit 9089ceb

Browse files
committed
Merge branch 'master' into 971_calc_exp
2 parents 54a8f11 + 1443db1 commit 9089ceb

15 files changed

+868
-444
lines changed

buildSrc/src/main/kotlin/Releases.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ object Releases {
5353

5454
object DataCapture : LibraryArtifact {
5555
override val artifactId = "data-capture"
56-
override val version = "0.1.0-beta04"
56+
override val version = "0.1.0-beta05"
5757
override val name = "Android FHIR Structured Data Capture Library"
5858
}
5959

datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ internal fun Questionnaire.QuestionnaireItemComponent.isReferencedBy(
103103
item: Questionnaire.QuestionnaireItemComponent
104104
) =
105105
item.expressionBasedExtensions.any {
106-
it.castToExpression(it.value).expression.contains("'${this.linkId}'")
106+
it.castToExpression(it.value).expression.replace(" ", "").contains(Regex(".*linkId='${this.linkId}'.*"))
107107
}
108108

109109
// Item control code, or null
@@ -330,7 +330,7 @@ fun QuestionnaireResponse.QuestionnaireResponseItemComponent.addNestedItemsToAns
330330
*/
331331
fun List<Questionnaire.QuestionnaireItemComponent>.flattened():
332332
List<Questionnaire.QuestionnaireItemComponent> {
333-
return this + this.flatMap { if (it.hasItem()) it.item.flattened() else it.item }
333+
return this + this.flatMap { it.item.flattened() }
334334
}
335335

336336
/**

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt

+5-10
Original file line numberDiff line numberDiff line change
@@ -387,16 +387,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
387387
questionnaireResponseItemPreOrderList.find { it.linkId == updatedCalculable.first.linkId }
388388

389389
val evaluatedAnswer = updatedCalculable.second
390-
val currentAnswer = updatedCalculableResponse?.answer?.map { it.value }
391-
392-
// update and notify only if answer has changed to prevent any event loop
393-
// if current and previous both are not empty and the answer is changed in count or
394-
// content
395-
if ((evaluatedAnswer + currentAnswer).isNotEmpty() &&
396-
(evaluatedAnswer != currentAnswer ||
397-
evaluatedAnswer
398-
.filterIndexed { i, v -> currentAnswer[i].equalsDeep(v).not() }
399-
.isEmpty())
390+
val currentAnswer = updatedCalculableResponse?.answer?.map { it.value } ?: emptyList()
391+
392+
// update and notify only if new answer has changed to prevent any event loop
393+
if (evaluatedAnswer.size != currentAnswer.size ||
394+
evaluatedAnswer.zip(currentAnswer).any { (v1, v2) -> v1.equalsDeep(v2).not() }
400395
) {
401396
updatedCalculableResponse?.let {
402397
it.answer =

datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt

+16-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture.fhirpath
1919
import ca.uhn.fhir.context.FhirContext
2020
import ca.uhn.fhir.context.FhirVersionEnum
2121
import com.google.android.fhir.datacapture.calculatedExpression
22+
import com.google.android.fhir.datacapture.expressionBasedExtensions
2223
import com.google.android.fhir.datacapture.findVariableExpression
2324
import com.google.android.fhir.datacapture.flattened
2425
import com.google.android.fhir.datacapture.isReferencedBy
@@ -85,6 +86,17 @@ object ExpressionEvaluator {
8586
}
8687
}
8788

89+
/** Detects if any item into list is referencing a dependent item in its calculated expression */
90+
internal fun extractExpressionReferenceMap(
91+
items: List<Questionnaire.QuestionnaireItemComponent>
92+
) {
93+
items.flattened().filter { it.expressionBasedExtensions.isNotEmpty() }.run {
94+
forEach { current ->
95+
96+
}
97+
}
98+
}
99+
88100
/**
89101
* Returns a pair of item and the calculated and evaluated value for all items with calculated
90102
* expression extension, which is dependent on value of updated response
@@ -101,7 +113,10 @@ object ExpressionEvaluator {
101113
.item
102114
.flattened()
103115
.filter { item ->
104-
// item is calculable and not modified yet and depends on the updated answer
116+
// 1- item is calculable
117+
// 2- item answer is not modified and touched by user;
118+
// https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html
119+
// 3- item answer depends on the updated item answer
105120
item.calculatedExpression != null &&
106121
modifiedResponses.none { it.linkId == item.linkId } &&
107122
updatedQuestionnaireItem.isReferencedBy(item)

datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt

+39
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import org.hl7.fhir.r4.model.CodeType
3737
import org.hl7.fhir.r4.model.CodeableConcept
3838
import org.hl7.fhir.r4.model.Coding
3939
import org.hl7.fhir.r4.model.DecimalType
40+
import org.hl7.fhir.r4.model.DomainResource
4041
import org.hl7.fhir.r4.model.Enumeration
4142
import org.hl7.fhir.r4.model.Expression
4243
import org.hl7.fhir.r4.model.Extension
@@ -444,9 +445,47 @@ object ResourceMapper {
444445
.javaClass
445446
.getMethod("setValue", Type::class.java)
446447
.invoke(base, questionnaireResponseItem.answer.singleOrNull()?.value)
448+
return
447449
} catch (e: NoSuchMethodException) {
448450
// Do nothing
449451
}
452+
453+
if (base.javaClass.getFieldOrNull(fieldName) == null) {
454+
// If field not found in resource class, assume this is an extension
455+
addDefinitionBasedCustomExtension(questionnaireItem, questionnaireResponseItem, base)
456+
}
457+
}
458+
}
459+
460+
/**
461+
* Adds custom extension for Resource.
462+
* @param questionnaireItem QuestionnaireItemComponent with details for extension
463+
* @param questionnaireResponseItem QuestionnaireResponseItemComponent for response value
464+
* @param base
465+
* - resource's Base class instance See
466+
* https://hapifhir.io/hapi-fhir/docs/model/profiles_and_extensions.html#extensions for more on
467+
* custom extensions
468+
*/
469+
private fun addDefinitionBasedCustomExtension(
470+
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
471+
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
472+
base: Base
473+
) {
474+
if (base is Type) {
475+
// Create an extension
476+
val ext = Extension()
477+
ext.url = questionnaireItem.definition
478+
ext.setValue(questionnaireResponseItem.answer.first().value)
479+
// Add the extension to the resource
480+
base.addExtension(ext)
481+
}
482+
if (base is DomainResource) {
483+
// Create an extension
484+
val ext = Extension()
485+
ext.url = questionnaireItem.definition
486+
ext.setValue(questionnaireResponseItem.answer.first().value)
487+
// Add the extension to the resource
488+
base.addExtension(ext)
450489
}
451490
}
452491

datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt

+40-16
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import java.time.ZoneId
4141
import java.time.chrono.IsoChronology
4242
import java.time.format.DateTimeFormatterBuilder
4343
import java.time.format.FormatStyle
44+
import java.util.Date
4445
import java.util.Locale
4546
import kotlin.math.abs
4647
import kotlin.math.log10
@@ -103,16 +104,22 @@ internal object QuestionnaireItemDatePickerViewHolderFactory :
103104
textInputLayout.hint = localePattern
104105
textInputEditText.removeTextChangedListener(textWatcher)
105106

106-
textInputEditText.setText(
107-
questionnaireItemViewItem
108-
.answers
109-
.singleOrNull()
110-
?.takeIf { it.hasValue() }
111-
?.valueDateType
112-
?.localDate
113-
?.localizedString
114-
)
115-
107+
if (isTextUpdateRequired(
108+
textInputEditText.context,
109+
questionnaireItemViewItem.answers.singleOrNull()?.valueDateType,
110+
textInputEditText.text.toString()
111+
)
112+
) {
113+
textInputEditText.setText(
114+
questionnaireItemViewItem
115+
.answers
116+
.singleOrNull()
117+
?.takeIf { it.hasValue() }
118+
?.valueDateType
119+
?.localDate
120+
?.localizedString
121+
)
122+
}
116123
textWatcher = textInputEditText.doAfterTextChanged { text -> updateAnswer(text.toString()) }
117124
}
118125

@@ -159,6 +166,20 @@ internal object QuestionnaireItemDatePickerViewHolderFactory :
159166
}
160167
}
161168
}
169+
170+
private fun isTextUpdateRequired(
171+
context: Context,
172+
answer: DateType?,
173+
inputText: String?
174+
): Boolean {
175+
val inputDate =
176+
try {
177+
parseDate(inputText, context)
178+
} catch (e: Exception) {
179+
null
180+
}
181+
return answer?.localDate != inputDate
182+
}
162183
}
163184

164185
internal const val TAG = "date-picker"
@@ -196,14 +217,17 @@ internal val DateType.localDate
196217
internal val LocalDate.dateType
197218
get() = DateType(year, monthValue - 1, dayOfMonth)
198219

220+
internal val Date.localDate
221+
get() = LocalDate.of(year + 1900, month + 1, date)
222+
199223
internal fun parseDate(text: CharSequence?, context: Context): LocalDate {
200-
val date =
224+
val localDate =
201225
if (isAndroidIcuSupported()) {
202-
DateFormat.getDateInstance(DateFormat.SHORT).parse(text.toString())
203-
} else {
204-
android.text.format.DateFormat.getDateFormat(context).parse(text.toString())
205-
}
206-
val localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
226+
DateFormat.getDateInstance(DateFormat.SHORT).parse(text.toString())
227+
} else {
228+
android.text.format.DateFormat.getDateFormat(context).parse(text.toString())
229+
}
230+
.localDate
207231
// date/localDate with year more than 4 digit throws data format exception if deep copy
208232
// operation get performed on QuestionnaireResponse,
209233
// QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent in org.hl7.fhir.r4.model

datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt

+36-2
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory :
146146
val dateTime = questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType
147147
updateDateTimeInput(
148148
dateTime?.let {
149-
LocalDateTime.of(it.year, it.month + 1, it.day, it.hour, it.minute, it.second)
149+
it.localDateTime.also {
150+
localDate = it.toLocalDate()
151+
localTime = it.toLocalTime()
152+
}
150153
}
151154
)
152155
textWatcher =
@@ -197,7 +200,12 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory :
197200
/** Update the date and time input fields in the UI. */
198201
private fun updateDateTimeInput(localDateTime: LocalDateTime?) {
199202
enableOrDisableTimePicker(enableIt = localDateTime != null)
200-
if (dateInputEditText.text.isNullOrEmpty()) {
203+
if (isTextUpdateRequired(
204+
dateInputEditText.context,
205+
localDateTime,
206+
dateInputEditText.text.toString()
207+
)
208+
) {
201209
dateInputEditText.setText(localDateTime?.localizedDateString ?: "")
202210
}
203211
timeInputEditText.setText(
@@ -281,6 +289,21 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory :
281289
timeInputLayout.isEnabled = enableIt
282290
timeInputLayout.isEnabled = enableIt
283291
}
292+
293+
private fun isTextUpdateRequired(
294+
context: Context,
295+
answer: LocalDateTime?,
296+
inputText: String?
297+
): Boolean {
298+
val inputDate =
299+
try {
300+
generateLocalDateTime(parseDate(inputText, context), localTime)
301+
} catch (e: Exception) {
302+
null
303+
}
304+
if (answer == null || inputDate == null) return true
305+
return answer.toLocalDate() != inputDate.toLocalDate()
306+
}
284307
}
285308
}
286309

@@ -301,3 +324,14 @@ internal val DateTimeType.localTime
301324
minute,
302325
second,
303326
)
327+
328+
internal val DateTimeType.localDateTime
329+
get() =
330+
LocalDateTime.of(
331+
year,
332+
month + 1,
333+
day,
334+
hour,
335+
minute,
336+
second,
337+
)

datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemRadioGroupViewHolderFactory.kt

+5-10
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,11 @@ internal object QuestionnaireItemRadioGroupViewHolderFactory :
141141
}
142142

143143
private fun updateAnswer(answerOption: Questionnaire.QuestionnaireItemAnswerOptionComponent) {
144-
// if-else block to prevent over-writing of "items" nested within "answer"
145-
if (questionnaireItemViewItem.answers.isNotEmpty()) {
146-
questionnaireItemViewItem.answers.apply { this[0].value = answerOption.value }
147-
} else {
148-
questionnaireItemViewItem.setAnswer(
149-
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
150-
value = answerOption.value
151-
}
152-
)
153-
}
144+
questionnaireItemViewItem.setAnswer(
145+
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
146+
value = answerOption.value
147+
}
148+
)
154149
}
155150
}
156151
}

0 commit comments

Comments
 (0)