Stream: fhirpath
Topic: Finding index of element using FP?
Patrick Narkinsky (Jun 07 2021 at 12:28):
Hi there. We've been working on a fhirpatch implementation for Javascript (which we're open sourcing). We're using the fhirpath.js
module to handle the fhirpath's. In order to implement the delete operator for a path like this:
Practitioner.telecom.where(value='5551212')
Our current implementation tries to extract the index of the matching list entry. However, we've tried a couple of different options and neither is producing the desired result:
1) Practitioner.telecom.where(value='5551212').$index
- returns the length of the array
2) Practitioner.telecom.where(value='5551212').select($index)
- seems to return the number of matching elements in the array minus 1 (i.e. the index of the last matching element in the reduced list.)
Is there any robust way in FHIRPath to implement this? This is critical functionality for our use of fhirpatch, and we're trying to avoid having to write our own fhirpath implementation if we possibly can.
Grahame Grieve (Jun 07 2021 at 14:33):
@Bryn Rhodes I don't believe we've made any definition for $index outside an iterator, but it's use here would be very difficult, since the index would be the index in the .where() not the index in .telecom.
I think we'd have to define an indexOf() function on a list, and it seems like an obvious oversight to me
Grahame Grieve (Jun 07 2021 at 14:34):
in the meantime, patrick you can define your own additional functions no problem
Paul Lynch (Jun 07 2021 at 14:38):
Why do you want the index? What are you planning to use it for?
Patrick Narkinsky (Jun 07 2021 at 18:12):
Paul Lynch said:
Why do you want the index? What are you planning to use it for?
I started to try to explain this, but I think the source for our delete operation is clearer than any English explanation.
From https://github.com/caremesh/fhirpatch/blob/main/src/operation.js
case 'delete':
// if the tail path contains an operation, patch it to be an absolute index
if (this.tail.path.startsWith('where\(')) {
const [idx] = fp.evaluate(resource, `${this.path}.$index`);
if (typeof idx === 'undefined') {
break;
}
this.path = `${this.containingPath}[${idx}]`;
}
res = fp.evaluate(resource, this.containingPath);
if (res[0]) {
// it is not an error if a path to delete doesn't already exist
if (this.tail.index) {
if (res.length == 0) {
throw new PathNotFoundError(
`Attempt to modify index but path ${this.containingPath
} is not array`);
}
res[0][this.tail.path].splice(this.tail.index, 1);
} else {
delete res[0][this.tail.path];
}
}
break;
Granted this is a bit of a hack, but the idea here is that we need to delete the path referenced by the patch from the container. So, we use fhirpath to get the parent (by tokenizing the path into the containingPath
and the tail
with an optional index) then we remove the referenced element from the parent. However, our whole use case for this requires us to be able to delete accurately when the list order changes (long story why) so we can't do something like delete Practitioner.telecom[0]
-- we need to be able to delete by value.
That's why we decided to implement fhirpatch rather than try to use jsonpatch -- with jsonpatch there was just no apparent way to support this use case.
Patrick Narkinsky (Jun 07 2021 at 18:17):
Grahame Grieve said:
Bryn Rhodes I don't believe we've made any definition for $index outside an iterator, but it's use here would be very difficult, since the index would be the index in the .where() not the index in .telecom.
I think we'd have to define an indexOf() function on a list, and it seems like an obvious oversight to me
@Grahame Grieve am I correct in understanding that implementing the indexOf function would best be done within our fhirpath implementation (fhirpath.js)? Or is there some magic I'm not aware of?
Grahame Grieve (Jun 07 2021 at 18:33):
oh you'd have to do it in your implementation - there's no magic like getting an implementation for free ;-)
Paul Lynch (Jun 07 2021 at 18:33):
It seems like support for the FHIRPatch operations should be in FHIRPath.
Paul Lynch (Jun 07 2021 at 18:37):
@Patrick Narkinsky You can fork fhirpath.js and add the indexOf function, which I guess should take an expression. Alternativly, you could add a delete() function. Simultaneously, I would recommend filing a ticket on jira.hl7.org to add the missing functionality to the specification.
Patrick Narkinsky (Jun 07 2021 at 18:38):
@Paul Lynch since I've had no experience with ANTLR, I was trying to avoid getting deep into fhirpath. Oh well...
Paul Lynch (Jun 07 2021 at 18:42):
@Patrick Narkinsky I will PM you with some advice about the code-- you should not need to know ANTLR.
Patrick Narkinsky (Jun 07 2021 at 21:17):
Thanks folks! With @Paul Lynch 's help, I've got a working implementation. I'm going to clean it up and then offer a pull request. I will probably also significantly refactor our fhirpatch library now that I've seen how easy it is to modify fhirpath.js.
Paul Lynch (Jun 07 2021 at 21:40):
@Grahame Grieve Where you thinking indexOf would take an expression, like "where", as in Patient.name.indexOf(use='official')
?
Grahame Grieve (Jun 07 2021 at 21:49):
For me. more like this:
Patient.name.indexOf(Patient.name.where(use='official'))
Grahame Grieve (Jun 07 2021 at 21:49):
there's obvious problems with that
Paul Lynch (Jun 07 2021 at 21:50):
If you just put the expression into the indexOf, the function can be the same as "where" except it returns the index instead of the object.
Grahame Grieve (Jun 07 2021 at 21:51):
maybe but it isn't it less re-usable?
Paul Lynch (Jun 07 2021 at 21:52):
I'm not following. Re-usable in what sense?
Paul Lynch (Jun 07 2021 at 21:55):
Oh, I see. You want to be able to look up the index based on the object, rather than the expression that finds it. I suppose, but is twice the work if you are starting with an expression (first you find the object, and then you search for it again to find the index).
Grahame Grieve (Jun 07 2021 at 21:55):
look up the index based on the object
yes.
Paul Lynch (Jun 07 2021 at 22:01):
Testing for equality could be really inefficient if you have a complex object. (Bundle.entry.indexOf(...)).
Paul Lynch (Jun 07 2021 at 22:04):
It is too bad we can't pass in either an object or an expression. Maybe we need two functions.
Grahame Grieve (Jun 07 2021 at 22:10):
It is too bad we can't pass in either an object or an expression
can't we?
Paul Lynch (Jun 07 2021 at 23:06):
I don't think we currently have such a case. fhirpath.js would not handle it, but could be modified if the grammar could support it.
Paul Lynch (Jun 07 2021 at 23:46):
It might be possible to check whether what is passed into indexOf is a TermExpression (from the grammar-- this would mean an object or value is being passed in).
Grahame Grieve (Jun 08 2021 at 01:59):
well we haven't said that couldn't happen, but we do differentiate between iterators and everything else
Bryn Rhodes (Jun 08 2021 at 18:49):
Hmm... yes, definitely should have an IndexOf and agree it is an oversight. We could do it with the $index though:
Practitioner.telecom.select(iff(value='5551212', $index, { }))
Bryn Rhodes (Jun 08 2021 at 18:53):
Curious if that would work in fhirpath.js Paul?
Bryn Rhodes (Jun 08 2021 at 18:53):
https://jira.hl7.org/browse/FHIR-32882
Bryn Rhodes (Jun 08 2021 at 18:53):
And a tracker to remedy the oversight.
Paul Lynch (Jun 08 2021 at 20:47):
Bryn Rhodes said:
Curious if that would work in fhirpath.js Paul?
Yes, it does!
Grahame Grieve (Jun 14 2021 at 07:58):
that works in the java implementation when I commit it (I overlooked $index)
Last updated: Apr 12 2022 at 19:14 UTC