-
Notifications
You must be signed in to change notification settings - Fork 250
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
Add enablement operators EQUALS and NOT_EQUAL_TO #239
Add enablement operators EQUALS and NOT_EQUAL_TO #239
Conversation
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
responseItem: QuestionnaireResponse.Item, | ||
enableWhen: Questionnaire.Item.EnableWhen | ||
) = | ||
responseItem.answerList.map { it.value.toByteString() }.contains( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a better way to equality? values
with the same content are not equal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm I think this doesn't equal because they're different classes (Questionnaire.Item.EnableWhen.AnswerX
and QuestionnaireResponse.Item.Answer.ValueX
)... The right way of doing this probably write a utility function to compare each data type (since we don't actually know the type here). I'm happy for that to be a follow-up (or just create an issue)
datacapture/src/main/java/com/google/android/fhir/datacapture/enablement/EnablementEvaluator.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Peter for your contribution 👍 just a couple of high-level comments. Haven't looked through the tests yet.
*/ | ||
private fun responseItemContainsAnswer( | ||
responseItem: QuestionnaireResponse.Item, | ||
enableWhen: Questionnaire.Item.EnableWhen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feels like this function signature can be tighter with the answer field in enableWhen
rather than enableWhen
itself.
@@ -105,7 +105,24 @@ internal object EnablementEvaluator { | |||
return when (val operator = enableWhen.operator.value) { | |||
QuestionnaireItemOperatorCode.Value.EXISTS -> | |||
(responseItem.answerCount > 0) == enableWhen.answer.boolean.value | |||
QuestionnaireItemOperatorCode.Value.EQUALS -> | |||
responseItemContainsAnswer(responseItem, enableWhen) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at rest of the opeartors: https://www.hl7.org/fhir/valueset-questionnaire-enable-operator.html it probably would make a lot of sense to provide some kind of predicate function here.
so you'd have in here:
responseItem.contains(enableWhen.operator.toPredicate())
and then have two extension functions.
the first extension function converts an enable when operator to a predicate
private fun Questionnaire.Item.EnableWhen.OperatorCode.toPredicate(): (QuestionnaireResponse.Item.Answer) -> Boolean {
...
}
and the second extension function takes that predicate to figure out if any answer in the answer list in the response item satisfies that predicate:
private fun QuestionnaireResponse.Item.contains(predicate:(QuestionnaireResponse.Item.Answer) -> Boolean) {
...
}
I think as a result this when
block will be moved to the toPredicate
extension function, and the code in this function will be simplified.
responseItem: QuestionnaireResponse.Item, | ||
enableWhen: Questionnaire.Item.EnableWhen | ||
) = | ||
responseItem.answerList.map { it.value.toByteString() }.contains( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm I think this doesn't equal because they're different classes (Questionnaire.Item.EnableWhen.AnswerX
and QuestionnaireResponse.Item.Answer.ValueX
)... The right way of doing this probably write a utility function to compare each data type (since we don't actually know the type here). I'm happy for that to be a follow-up (or just create an issue)
758299e
to
31e2990
Compare
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
else -> throw NotImplementedError("Enable when operator $operator is not implemented.") | ||
} | ||
return if (QuestionnaireItemOperatorCode.Value.EXISTS == enableWhen.operator.value) | ||
(responseItem.answerCount > 0) == enableWhen.answer.boolean.value else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this operator seems like a special case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it work if you just have a
{ enableWhen.answer.boolean.value }
So a predicate that would always return true
for any answer item if the enableWhen.answer.boolean.value
is true
. And one that always returns false
for any answer item if the enableWhen.aswer.boolean.value
is false
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yea, so I had tried that, unless I'm missing something the issue is the evaluate_expectsNoAnswer_answerDoesNotExist_shouldReturnTrue
test case where enable when exists is set to false and there are no answer, this should return true, but because there are no answers it will return false.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm. yeah. damn. 😛
nit: can you please just reformat this a little bit
return if (...) {
...
}
else {
...
}
/** | ||
* Returns a predicate based on the `EnableWhen` `operator` and `Answer` value. | ||
*/ | ||
private fun Questionnaire.Item.EnableWhen.toPredicate(): (QuestionnaireResponse.Item.Answer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because the predicate depends on the value of the EnableWhen.Answer, I wrote this extending EnableWhen instead of EnableWhen.operator. This could take the answer as an additional argument, hmm maybe there's a way to clean this up
when (val operator = this.operator.value) { | ||
QuestionnaireItemOperatorCode.Value.EQUALS -> | ||
return { | ||
this.answer.toByteString() == it.value.toByteString() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda suspicious, I'm sure I'm missing a better way with the similarity here to line 123.
I looked a bit at how to write the type aware comparisons, having this return only the operator could help to remove the need for the answer here (but then the answer would need to be passed to contains I think) and the type aware comparisons could go in contains, using the operator.
Or, and probably better, have a helper here that takes the operator and the answer, then runs the type aware comparisons
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
d2bd7ce
to
451d977
Compare
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
451d977
to
25d33aa
Compare
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
@@ -0,0 +1,52 @@ | |||
package com.google.android.fhir.datacapture |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there better name for this file? not sure how best to name structure in kotlin projects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah in Kotlin you typically do More*s.kt for utilities. So maybe MoreQuestionnaireItemTypes.kt
? I feel it's ok to keep these two in the same class even though one is about questionnaire item and the other is about questionnaire response item.
25d33aa
to
23260d4
Compare
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Peter!
@@ -0,0 +1,52 @@ | |||
package com.google.android.fhir.datacapture |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah in Kotlin you typically do More*s.kt for utilities. So maybe MoreQuestionnaireItemTypes.kt
? I feel it's ok to keep these two in the same class even though one is about questionnaire item and the other is about questionnaire response item.
* TODO: move to a shared library with QuestionnaireResponse.Item.Answer.getValueForType | ||
* Returns the value of the [Questionnaire.Item.EnableWhen.AnswerX] for the [type]. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove
} catch (exception: IllegalArgumentException) { | ||
this.answer.toByteString() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel slightly uneasy about swallowing an error this way. Can we just throw?
same below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea sounds good, will update the tests to set the questionnaire item type and remove
* | ||
* @param type used to get value based on [Questionnaire.Item.TypeCode]. | ||
*/ | ||
private fun Questionnaire.Item.EnableWhen.toPredicate(type: Questionnaire.Item.TypeCode): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the comment i think you're right here about not putting the extension function on the operator and my initial suggestion was probably misleading. also seems kinda strange to me to actually have to have the type as a param. on 2nd thought perhaps this isn't really meant to be an extension function after all? happy for this to be a just private utility function. what do you think?
else -> throw NotImplementedError("Enable when operator $operator is not implemented.") | ||
} | ||
return if (QuestionnaireItemOperatorCode.Value.EXISTS == enableWhen.operator.value) | ||
(responseItem.answerCount > 0) == enableWhen.answer.boolean.value else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it work if you just have a
{ enableWhen.answer.boolean.value }
So a predicate that would always return true
for any answer item if the enableWhen.answer.boolean.value
is true
. And one that always returns false
for any answer item if the enableWhen.aswer.boolean.value
is false
.
} | ||
|
||
@Test | ||
fun evaluate_expectsAnswer_answerEqualOne_shouldReturnTrue() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the way i think about these test cases is that: we'll cover each operator in positive and negative cases, and then we'll have some tests that cover cases with multiple answers regardless of operators. having these cases is fine, but i kinda feel they'll really make the number of test cases explode without improving test coverage (since all cases should already have been hit).
as an example: we also have the ANY / OR enable when behaviors, does it mean the number of tests cases will x2? I don't think that'd be manageable.
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
dbb75e9
to
5dd7c4c
Compare
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉🎉🎉
approved. just a few nits. thanks again peter!
/** | ||
* Returns the value of the [Questionnaire.Item.EnableWhen.AnswerX] for the [type]. | ||
* | ||
* Used to retrieve the value to set the field in the extracted FHIR resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this line can be removed since it's now a utility function used for various things.
/** | ||
* Returns the value of the [QuestionnaireResponse.Item.Answer] for the [type]. | ||
* | ||
* Used to retrieve the value to set the field in the extracted FHIR resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and this.
else -> throw NotImplementedError("Enable when operator $operator is not implemented.") | ||
} | ||
return if (QuestionnaireItemOperatorCode.Value.EXISTS == enableWhen.operator.value) | ||
(responseItem.answerCount > 0) == enableWhen.answer.boolean.value else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm. yeah. damn. 😛
nit: can you please just reformat this a little bit
return if (...) {
...
}
else {
...
}
enableWhen: Questionnaire.Item.EnableWhen, | ||
type: Questionnaire.Item.TypeCode | ||
): | ||
(QuestionnaireResponse.Item.Answer) -> Boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this can be on the previous line
return { | ||
val answerValue = it.getValueForType(type) | ||
when (val operator = enableWhen.operator.value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think the structure here should be changed. here a function closure is returned... and the branching of operator happens when the function closure predicate is called. in other words if you need to evaluate multiple answers, each time you check which operator and choose the correct comparison logic. that seems logically redundant and an (admitedly tiny tiny) performance penalty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool yea definitely makes sense to keep the closure tight, there's probably room later for a further refactor here to get rid of some redundancy, was looking at the compareTo
function, but couldn't get to a better place, will push in
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
i'm not sure this bot is working... |
it seems to think your email address is [email protected] for some reason... |
Oh yea that's correct w/the email privacy option on, let me turn it off and rebase my commits to add my email... |
1d1143e
to
529880c
Compare
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
should be set to [email protected] now |
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed (or fixed any issues), please reply here with What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
from what I can see [email protected] is still in the state of |
@googlebot I signed it! |
CLAs look good, thanks! ℹ️ Googlers: Go here for more info. |
Closes #236
Based on Valueset-questionnaire-enable-operator
EQUALS
is true if any answers contain theenableWhen
answer. The definition of Not Equals is oddly worded, coded asNOT_EQUAL_TO
is the negation ofEQUALS
.