Skip to content

Commit

Permalink
Merge pull request #36 in LFOR/fhirpath.js from feature/LF-2171/inter…
Browse files Browse the repository at this point in the history
…sect to master

* commit '1a24c742108781275e6721696ce5738713ffa60d':
  Updated CHANGELOG.md
  Minor fixes
  Fixed issues found during review
  union(), distinct() and intersect() should not depend on the order of properties in an object
  Support intersect()
  • Loading branch information
yuriy-sedinkin committed Mar 21, 2022
2 parents f4178a2 + 1a24c74 commit 36290bf
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 73 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [2.14.0] - 2022-03-02
### Added
- Function to get the intersection of two collections: intersect().
### Fixed
- The distinct, union, subsetOf, and intersect functions now use
the "6.1.1. = (Equals)" function to compare collection items instead of using
a map with JSON keys, which can affect their performance because the
complexity of the algorithm has changed from O(n) to O(n**2).

## [2.13.0] - 2022-02-28
### Added
- Current time function: timeOfDay().
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fhirpath",
"version": "2.13.0",
"version": "2.14.0",
"description": "A FHIRPath engine",
"main": "src/fhirpath.js",
"dependencies": {
Expand Down
16 changes: 14 additions & 2 deletions src/combining.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// This file holds code to hande the FHIRPath Combining functions.

var combineFns = {};
var existence = require('./existence');
const combineFns = {};
const existence = require('./existence');
const deepEqual = require('./deep-equal');

combineFns.union = function(coll1, coll2){
return existence.distinctFn(coll1.concat(coll2));
Expand All @@ -11,5 +12,16 @@ combineFns.combineFn = function(coll1, coll2){
return coll1.concat(coll2);
};

combineFns.intersect = function(coll1, coll2) {
let result = [];
if (coll1.length && coll2.length) {
result = existence.distinctFn(coll1).filter(
obj1 => coll2.some(obj2 => deepEqual(obj1, obj2))
);
}

return result;
};


module.exports = combineFns;
6 changes: 1 addition & 5 deletions src/deep-equal.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,8 @@ var deepEqual = function (actual, expected, opts) {

if (actual instanceof Date && expected instanceof Date) {
return actual.getTime() === expected.getTime();

// 7.3. Other pairs that do not both pass typeof value == 'object',
// equivalence is determined by ==.
} else if (!actual || !expected || typeof actual != 'object' && typeof expected != 'object') {
return opts.strict ? actual === expected : actual == expected;

return actual === expected;
}
else {
var actualIsFPT = actual instanceof FP_Type;
Expand Down
68 changes: 8 additions & 60 deletions src/existence.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const util = require("./utilities");
const filtering = require("./filtering");
const misc = require("./misc");
const deepEqual = require('./deep-equal');

const engine = {};
engine.emptyFn = util.isEmpty;
Expand Down Expand Up @@ -68,62 +69,15 @@ engine.anyFalseFn = function(x) {
};


/**
* Returns a JSON version of the given object, but with keys of the object in
* sorted order (or at least a stable order).
* From: https://stackoverflow.com/a/35810961/360782
*/
function orderedJsonStringify(obj) {
return JSON.stringify(sortObjByKey(obj));
}

/**
* If given value is an object, returns a new object with the properties added
* in sorted order, and handles nested objects. Otherwise, returns the given
* value.
* From: https://stackoverflow.com/a/35810961/360782
*/
function sortObjByKey(value) {
return (typeof value === 'object') ?
(Array.isArray(value) ?
value.map(sortObjByKey) :
Object.keys(value).sort().reduce(
(o, key) => {
const v = value[key];
o[key] = sortObjByKey(v);
return o;
}, {})
) :
value;
}


/**
* Returns true if coll1 is a subset of coll2.
*/
function subsetOf(coll1, coll2) {
let rtn = coll1.length <= coll2.length;
if (rtn) {
// This requires a deep-equals comparision of every object in coll1,
// against each object in coll2.
// Optimize by building a hashmap of JSON versions of the objects.
var c2Hash = {};
for (let p=0, pLen=coll1.length; p<pLen && rtn; ++p) {
let obj1 = util.valData(coll1[p]);
let obj1Str = orderedJsonStringify(obj1);
let found = false;
if (p===0) { // c2Hash is not yet built
for (let i=0, len=coll2.length; i<len; ++i) {
// No early return from this loop, because we're building c2Hash.
let obj2 = util.valData(coll2[i]);
let obj2Str = orderedJsonStringify(obj2);
c2Hash[obj2Str] = obj2;
found = found || (obj1Str === obj2Str);
}
}
else
found = !!c2Hash[obj1Str];
rtn = found;
rtn = coll2.some(obj2 => deepEqual(obj1, util.valData(obj2)));
}
}
return rtn;
Expand All @@ -143,19 +97,13 @@ engine.isDistinctFn = function(x) {

engine.distinctFn = function(x) {
let unique = [];
// Since this requires a deep equals, use a hash table (on JSON strings) for
// efficiency.
if (x.length > 0) {
let uniqueHash = {};
for (let i=0, len=x.length; i<len; ++i) {
let xObj = x[i];
let xStr = JSON.stringify(xObj);
let uObj = uniqueHash[xStr];
if (uObj === undefined) {
unique.push(xObj);
uniqueHash[xStr] = xObj;
}
}
x = x.concat();
do {
let xObj = x.shift();
unique.push(xObj);
x = x.filter(o => !deepEqual(xObj, o));
} while (x.length);
}
return unique;
};
Expand Down
1 change: 1 addition & 0 deletions src/fhirpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ engine.invocationTable = {
skip: {fn: filtering.skipFn, arity: {1: ["Integer"]}},
combine: {fn: combining.combineFn, arity: {1: ["AnyAtRoot"]}},
union: {fn: combining.union, arity: {1: ["AnyAtRoot"]}},
intersect: {fn: combining.intersect, arity: {1: ["AnyAtRoot"]}},
iif: {fn: misc.iifMacro, arity: {2: ["Expr", "Expr"], 3: ["Expr", "Expr", "Expr"]}},
trace: {fn: misc.traceFn, arity: {0: [], 1: ["String"]}},
toInteger: {fn: misc.toInteger},
Expand Down
4 changes: 2 additions & 2 deletions src/utilities.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file holds utility functions used in implementing the public functions.

var util = {};
var types = require('./types');
const util = {};
const types = require('./types');
let {ResourceNode} = types;

/**
Expand Down
2 changes: 1 addition & 1 deletion test/cases/3.2_paths.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ tests:
result: ['medium', 'low', 'zero']

- desc: "QR with where()"
expression: "contained.where(resourceType = 'QuestionnaireResponse').item.where(linkId = 1).answer.value"
expression: "contained.where(resourceType = 'QuestionnaireResponse').item.where(linkId = '1').answer.value"
model: 'r4'
result: ['Red']

Expand Down
19 changes: 19 additions & 0 deletions test/cases/5.1_existence.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,16 @@ tests:
expression: Functions.coll1[0].coll2.attr.distinct()
result: [1, 2, 3]

- desc: '** should not depend on the order of properties in an object'
expression: Functions.objects.distinct()
result:
- prop1: 1
prop2: 2
- prop1: 3
prop2: 4
- prop1: 5
prop2: 6


- desc: '5.1.13. count() : integer'
# Returns a collection with a single value which is
Expand Down Expand Up @@ -410,4 +420,13 @@ subject:
c:
e: 6
d: 5
objects:
- prop1: 1
prop2: 2
- prop1: 3
prop2: 4
- prop2: 2
prop1: 1
- prop1: 5
prop2: 6

18 changes: 18 additions & 0 deletions test/cases/5.3_subsetting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ tests:
expression: Functions.coll1.coll2.attr.take(5)
result: [1, 2, 3, 4, 5]

- desc: '5.3.8. intersect(other: collection) : collection'
- desc: '** should not depend on the order of properties in an object'
expression: Functions.objects.group1.intersect(Functions.objects.group2)
result:
- prop1: 1
prop2: 2

subject:
resourceType: Functions
attrempty: []
Expand Down Expand Up @@ -216,3 +223,14 @@ subject:
- attr: '@2015-02-04T14:34:28Z'
- attr: '@T14:34:28+09:00'
- attr: 4 days
objects:
group1:
- prop1: 1
prop2: 2
- prop1: 3
prop2: 4
group2:
- prop2: 2
prop1: 1
- prop1: 5
prop2: 6
20 changes: 20 additions & 0 deletions test/cases/5.4_combining.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ tests:
expression: Functions.attrdouble | Functions.coll1.coll2.attr
result: [1, 2, 3, 4, 5]

- desc: '** should not depend on the order of properties in an object'
expression: Functions.objects.group1 | Functions.objects.group2
result:
- prop1: 1
prop2: 2
- prop1: 3
prop2: 4
- prop1: 5
prop2: 6

- desc: '5.4.2. combine(other : collection) : collection'
# Merge the input and other collections into a single collection without eliminating duplicate values. Combining an empty collection with a non-empty collection will return the non-empty collection. There is no expectation of order in the resulting collection.
Expand Down Expand Up @@ -85,3 +94,14 @@ subject:
- attr: '@2015-02-04T14:34:28Z'
- attr: '@T14:34:28+09:00'
- attr: 4 days
objects:
group1:
- prop1: 1
prop2: 2
- prop1: 3
prop2: 4
group2:
- prop2: 2
prop1: 1
- prop1: 5
prop2: 6
1 change: 0 additions & 1 deletion test/cases/fhir-r4.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2833,7 +2833,6 @@ tests:
expression: 1.combine(1).intersect(1).count() = 1
result:
- true
disable: true
- 'group: testExclude':
- desc: '** testExclude1'
inputfile: patient-example.json
Expand Down

0 comments on commit 36290bf

Please sign in to comment.