Implement missing features from the latest version of the fhirpath spec#182
Implement missing features from the latest version of the fhirpath spec#182brianpos wants to merge 16 commits intoHL7:masterfrom
Conversation
| where: {fn: filtering.whereMacro, arity: {1: ["Expr"]}}, | ||
| extension: {fn: filtering.extension, arity: {1: ["String"]}}, | ||
| select: {fn: filtering.selectMacro, arity: {1: ["Expr"]}}, | ||
| coalesce: {fn: filtering.coalesce, arity: {1: ["ExprAtCurrent"], 2: ["ExprAtCurrent", "ExprAtCurrent"], 3: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"], 4: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"], 5: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"], 6: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"], 7: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"], 8: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"], 9: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"], 10: ["ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent", "ExprAtCurrent"]}}, |
There was a problem hiding this comment.
surely there must be a better way to handle an arbitrary number of arguments?
There was a problem hiding this comment.
There was in the Firely SDK, but that was a bigger scope than I was happy to make.
I'm happy to give that a whirl, but I thought this was a pragmatic answer that would work for "most" uses...
(same with Sort)
There was a problem hiding this comment.
Please go ahead. I think we need a new syntax here to express "1 or more of these things". Maybe we could borrow the + from regular expressions, and write:
coalesce: {fn: filtering.coalesce, arity: {1: ["ExprAtCurrent+"]}
Something with a + would need to be the last argument.
There was a problem hiding this comment.
Or:
coalesce: {fn: filtering.coalesce, arity: {*: ["ExprAtCurrent+"]}
There was a problem hiding this comment.
No, that needs a + too:
coalesce: {fn: filtering.coalesce, arity: {+: ["ExprAtCurrent+"]}
There was a problem hiding this comment.
I went a slightly different direction:
coalesce: {fn: filtering.coalesce, params: {argType: "ExprAtCurrent", minArgCount: 1 }},
sort: {fn: filtering.sort, params: {argType: "SortArgument"}},And then updated the doInvoke to handle it.
This is specific to only sort and coalesce so think its fine.
(sent screenshot of change to doInvoke on Zulip)
| - desc: '** yearOf for a Time value returns empty' | ||
| expression: (@T10:30:00).yearOf() | ||
| result: [] | ||
|
|
There was a problem hiding this comment.
How about a test for an inappropriate data type, like 5.yearOf()?
There was a problem hiding this comment.
Great find, this is also a gap in the spec!
https://jira.hl7.org/browse/FHIR-55946
| valA = null; | ||
| } | ||
| if (resultB.length > 0) { | ||
| util.assertOnlyOne(resultB, 'Sort expression must return singleton value'); |
There was a problem hiding this comment.
Similar actions are performed in compareValues->typecheck->util.assertOnlyOne later in this function
There was a problem hiding this comment.
My manual checking complains earlier I guess (and as a result gets a more specific error message)
I still call the type check to ensure that the types are valid though right?
There was a problem hiding this comment.
Performance is critical for evaluating FHIRPath expressions. If we can avoid repetition, we should avoid it.
| } | ||
|
|
||
| // Extract values for comparison | ||
| valA = valA !== null ? util.valData(valA) : null; |
There was a problem hiding this comment.
Similar actions are performed in compareValues->typecheck->util.valDataConverted later in this function.
Perhaps this block from // Enforce singleton requirement per specification to // Compare values using FHIRPath comparison semantics could be removed, and the compareValues function modified slightly?
There was a problem hiding this comment.
the usage in the compare values is kinda different to this part here, as in the compare values, that's handling the considerations of ordering where a value is null or not, where this part is determining if each value is null, and making that consistent. And then being able to flow down to the next sort parameter if required.
Null doesn't mean stop, it can mean look at the next parameter...
There was a problem hiding this comment.
The tests pass if I replace those two functions with the following code (it is shorter, but I guess it could still be improved for better performance):
engine.sort = function(data, ...sortArgs) {
if(data !== false && !data) { return []; }
// If no sort arguments provided, use natural ordering
if (sortArgs.length === 0) {
return data.slice().sort((a, b) => {
return compareValues([a], [b]);
});
}
// Sort with expressions and directions
return data.slice().sort((a, b) => {
for (let i = 0; i < sortArgs.length; i++) {
const sortArg = sortArgs[i];
const direction = sortArg.direction || 'asc';
// Evaluate the sort expression for both items
const resultA = sortArg.expr([a]);
const resultB = sortArg.expr([b]);
// Compare values using FHIRPath comparison semantics
let comparison = compareValues(resultA, resultB);
// Apply direction
if (direction === 'desc') {
comparison = -comparison;
}
// If this sort key produces a difference, return it
if (comparison !== 0) {
return comparison;
}
// Otherwise, continue to the next sort key
}
// All sort keys were equal
return 0;
});
};
/**
* Compare two values using FHIRPath comparison semantics
* Reuses existing equality.js comparison logic
*/
function compareValues(a, b) {
// Use existing FHIRPath comparison logic from equality.js
// Convert to singleton arrays for typecheck
const [a0, b0] = equality.typecheck(a, b);
// Handle empty values - per spec: "An empty value is considered lower than all other values"
if (a0 == null && b0 == null) return 0;
if (a0 == null) return -1; // Empty values sort before non-empty values
if (b0 == null) return 1;
// Handle FP_Type objects (dates, times, quantities, etc.)
if (a0 instanceof FP_Type) {
const compareResult = a0.compare(b0);
return compareResult === null ? 0 : compareResult;
}
// TODO: Tests passed without error if I comment out the following code:
// // Reject non-primitive types that can't be meaningfully compared
// // Per spec: "Values that would result in comparison errors must be filtered prior to sorting"
// let type = typeof a0;
// if (type === 'object' || type === 'function') {
// throw new Error('Cannot sort by non-primitive type: ' + (a0.constructor?.name || type));
// }
// type = typeof b0;
// if (type === 'object' || type === 'function') {
// throw new Error('Cannot sort by non-primitive type: ' + (b0.constructor?.name || type));
// }
// Standard JavaScript comparison for basic types
if (a0 === b0) return 0;
return a0 < b0 ? -1 : a0 > b0 ? 1 : 0;
}Do you think these changes look good?
If you can improve these changes, that would be great.
Otherwise, I will think about it before merging this pull request.
I commented out the block of code about "Reject non-primitive types that can't be meaningfully compared" and all tests passed. An additional unit test is needed for this block of code.
…mplementation (and fix typo)
…n associated clarification against the spec https://jira.hl7.org/browse/FHIR-55946)
| } | ||
|
|
||
| // Extract values for comparison | ||
| valA = valA !== null ? util.valData(valA) : null; |
There was a problem hiding this comment.
The tests pass if I replace those two functions with the following code (it is shorter, but I guess it could still be improved for better performance):
engine.sort = function(data, ...sortArgs) {
if(data !== false && !data) { return []; }
// If no sort arguments provided, use natural ordering
if (sortArgs.length === 0) {
return data.slice().sort((a, b) => {
return compareValues([a], [b]);
});
}
// Sort with expressions and directions
return data.slice().sort((a, b) => {
for (let i = 0; i < sortArgs.length; i++) {
const sortArg = sortArgs[i];
const direction = sortArg.direction || 'asc';
// Evaluate the sort expression for both items
const resultA = sortArg.expr([a]);
const resultB = sortArg.expr([b]);
// Compare values using FHIRPath comparison semantics
let comparison = compareValues(resultA, resultB);
// Apply direction
if (direction === 'desc') {
comparison = -comparison;
}
// If this sort key produces a difference, return it
if (comparison !== 0) {
return comparison;
}
// Otherwise, continue to the next sort key
}
// All sort keys were equal
return 0;
});
};
/**
* Compare two values using FHIRPath comparison semantics
* Reuses existing equality.js comparison logic
*/
function compareValues(a, b) {
// Use existing FHIRPath comparison logic from equality.js
// Convert to singleton arrays for typecheck
const [a0, b0] = equality.typecheck(a, b);
// Handle empty values - per spec: "An empty value is considered lower than all other values"
if (a0 == null && b0 == null) return 0;
if (a0 == null) return -1; // Empty values sort before non-empty values
if (b0 == null) return 1;
// Handle FP_Type objects (dates, times, quantities, etc.)
if (a0 instanceof FP_Type) {
const compareResult = a0.compare(b0);
return compareResult === null ? 0 : compareResult;
}
// TODO: Tests passed without error if I comment out the following code:
// // Reject non-primitive types that can't be meaningfully compared
// // Per spec: "Values that would result in comparison errors must be filtered prior to sorting"
// let type = typeof a0;
// if (type === 'object' || type === 'function') {
// throw new Error('Cannot sort by non-primitive type: ' + (a0.constructor?.name || type));
// }
// type = typeof b0;
// if (type === 'object' || type === 'function') {
// throw new Error('Cannot sort by non-primitive type: ' + (b0.constructor?.name || type));
// }
// Standard JavaScript comparison for basic types
if (a0 === b0) return 0;
return a0 < b0 ? -1 : a0 > b0 ? 1 : 0;
}Do you think these changes look good?
If you can improve these changes, that would be great.
Otherwise, I will think about it before merging this pull request.
I commented out the block of code about "Reject non-primitive types that can't be meaningfully compared" and all tests passed. An additional unit test is needed for this block of code.
Implemented the following features: