Skip to content
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

Merged
merged 13 commits into from
Feb 17, 2021

Conversation

pld
Copy link
Collaborator

@pld pld commented Feb 14, 2021

Closes #236

Based on Valueset-questionnaire-enable-operator EQUALS is true if any answers contain the enableWhen answer. The definition of Not Equals is oddly worded, coded as NOT_EQUAL_TO is the negation of EQUALS.

@google-cla
Copy link

google-cla bot commented Feb 14, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

responseItem: QuestionnaireResponse.Item,
enableWhen: Questionnaire.Item.EnableWhen
) =
responseItem.answerList.map { it.value.toByteString() }.contains(
Copy link
Collaborator Author

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

Copy link
Collaborator

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)

Copy link
Collaborator

@jingtang10 jingtang10 left a 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
Copy link
Collaborator

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)
Copy link
Collaborator

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(
Copy link
Collaborator

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)

@jingtang10 jingtang10 added this to In progress in Data capture library via automation Feb 14, 2021
@pld pld force-pushed the pl/enablement-operator-equals-not-equals branch from 758299e to 31e2990 Compare February 14, 2021 20:10
@google-cla
Copy link

google-cla bot commented Feb 14, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual 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
Copy link
Collaborator Author

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

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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)
Copy link
Collaborator Author

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()
Copy link
Collaborator Author

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

@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@pld pld force-pushed the pl/enablement-operator-equals-not-equals branch from d2bd7ce to 451d977 Compare February 15, 2021 04:05
@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@pld pld force-pushed the pl/enablement-operator-equals-not-equals branch from 451d977 to 25d33aa Compare February 15, 2021 15:11
@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@@ -0,0 +1,52 @@
package com.google.android.fhir.datacapture
Copy link
Collaborator Author

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

Copy link
Collaborator

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.

@pld pld force-pushed the pl/enablement-operator-equals-not-equals branch from 25d33aa to 23260d4 Compare February 15, 2021 15:21
@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

Copy link
Collaborator

@jingtang10 jingtang10 left a 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
Copy link
Collaborator

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.

Comment on lines 25 to 27
* TODO: move to a shared library with QuestionnaireResponse.Item.Answer.getValueForType
* Returns the value of the [Questionnaire.Item.EnableWhen.AnswerX] for the [type].
*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Comment on lines 138 to 140
} catch (exception: IllegalArgumentException) {
this.answer.toByteString()
}
Copy link
Collaborator

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

Copy link
Collaborator Author

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):
Copy link
Collaborator

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
Copy link
Collaborator

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() {
Copy link
Collaborator

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.

@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@pld pld force-pushed the pl/enablement-operator-equals-not-equals branch from dbb75e9 to 5dd7c4c Compare February 15, 2021 21:07
@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

Copy link
Collaborator

@jingtang10 jingtang10 left a 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.
Copy link
Collaborator

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.
Copy link
Collaborator

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
Copy link
Collaborator

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 {
Copy link
Collaborator

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

Comment on lines 140 to 142
return {
val answerValue = it.getValueForType(type)
when (val operator = enableWhen.operator.value) {
Copy link
Collaborator

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.

Copy link
Collaborator Author

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

@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@jingtang10
Copy link
Collaborator

i'm not sure this bot is working...

@jingtang10
Copy link
Collaborator

it seems to think your email address is [email protected] for some reason...

@pld
Copy link
Collaborator Author

pld commented Feb 15, 2021

Oh yea that's correct w/the email privacy option on, let me turn it off and rebase my commits to add my email...

@pld pld force-pushed the pl/enablement-operator-equals-not-equals branch from 1d1143e to 529880c Compare February 15, 2021 23:14
@google-cla
Copy link

google-cla bot commented Feb 15, 2021

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@pld
Copy link
Collaborator Author

pld commented Feb 15, 2021

should be set to [email protected] now

@googlebot
Copy link

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 @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@jingtang10
Copy link
Collaborator

from what I can see [email protected] is still in the state of need_sender_cla... urg i'm so sorry about this faff. can you check again?

@pld
Copy link
Collaborator Author

pld commented Feb 17, 2021

@googlebot I signed it!

@google-cla
Copy link

google-cla bot commented Feb 17, 2021

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

@pld pld merged commit a16fee4 into google:master Feb 17, 2021
Data capture library automation moved this from In progress to Done Feb 17, 2021
@pld pld deleted the pl/enablement-operator-equals-not-equals branch February 17, 2021 17:03
@Tarun-Bhardwaj Tarun-Bhardwaj added the type:bug Something isn't working label Sep 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:bug Something isn't working
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Support enablement operator EQUALS and NOT_EQUALS
4 participants