Skip to content

Commit eab5df2

Browse files
Merge branch 'bugfix/LF-2881/handling-of-empty-values-coming-from-null' into 'master'
Handling of empty values that came from nulls See merge request lfor/fhirpath.js!8
2 parents 5428ef8 + 555d715 commit eab5df2

32 files changed

Lines changed: 836 additions & 191 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
This log documents significant changes for each release. This project follows
44
[Semantic Versioning](http://semver.org/).
55

6+
## [3.10.5] - 2024-03-25
7+
### Fixed
8+
- Handling of empty values that came from nulls.
9+
610
## [3.10.4] - 2024-03-13
711
### Fixed
812
- hasValue() function previously only checked the data type of an input

demo/package-lock.json

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fhirpath",
3-
"version": "3.10.4",
3+
"version": "3.10.5",
44
"description": "A FHIRPath engine",
55
"main": "src/fhirpath.js",
66
"dependencies": {

src/aggregate.js

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,38 +24,58 @@ engine.countFn = function(x) {
2424
// Shortcut for "value.tail().aggregate($this+$total, value.first())" `
2525
engine.sumFn = function(data) {
2626
return engine.aggregateMacro.apply(this, [data.slice(1), ($this) => {
27-
return math.plus(util.arraify($this), util.arraify(this.$total));
27+
let x = util.arraify($this).filter(i => util.valData(i) != null);
28+
let y = util.arraify(this.$total).filter(i => util.valData(i) != null);
29+
if (x.length === 0 || y.length === 0) {
30+
return [];
31+
}
32+
return math.plus(x, y);
2833
}, data[0]]);
2934
};
3035

36+
/**
37+
* Shortcut for "[source collection].aggregate(iif($total.empty(), $this, iif($this [operator] $total, $this, $total)))"
38+
* Used for functions min() and max().
39+
* @param {Array} data - source collection
40+
* @param {Function} fn - operator function
41+
* @return {Array}
42+
*/
43+
function minMaxShortcutTemplate(data, fn) {
44+
let $total;
45+
if (data.length === 0 || util.valData(data[0]) == null) {
46+
$total = [];
47+
} else {
48+
$total = [data[0]];
49+
for (let i = 1; i < data.length; i++) {
50+
if (util.valData(data[i]) == null) {
51+
$total = [];
52+
break;
53+
}
54+
const $this = [data[i]];
55+
$total = util.isTrue(fn($this, $total)) ? $this : $total;
56+
}
57+
}
58+
return $total;
59+
}
60+
3161
// Shortcut for "value.aggregate(iif($total.empty(), $this, iif($this < $total, $this, $total)))"
3262
engine.minFn = function (data) {
33-
return engine.aggregateMacro.apply(this, [data, (curr) => {
34-
const $this = util.arraify(curr);
35-
const $total = util.arraify(this.$total);
36-
return util.isEmpty($total)
37-
? $this
38-
: equality.lt($this, $total) ? $this : $total;
39-
}]);
63+
return minMaxShortcutTemplate(data, equality.lt);
4064
};
4165

4266
// Shortcut for "value.aggregate(iif($total.empty(), $this, iif($this > $total, $this, $total)))"
4367
engine.maxFn = function (data) {
44-
return engine.aggregateMacro.apply(this, [data, (curr) => {
45-
const $this = util.arraify(curr);
46-
const $total = util.arraify(this.$total);
47-
return util.isEmpty($total)
48-
? $this
49-
: equality.gt($this, $total) ? $this : $total;
50-
}]);
68+
return minMaxShortcutTemplate(data, equality.gt);
5169
};
5270

5371
// Shortcut for "value.sum()/value.count()"
5472
engine.avgFn = function (data) {
55-
return math.div(
56-
util.arraify(engine.sumFn(data)),
57-
util.arraify(engine.countFn(data))
58-
);
73+
const x = util.arraify(engine.sumFn(data));
74+
const y = util.arraify(engine.countFn(data));
75+
if (x.length === 0 || y.length === 0) {
76+
return [];
77+
}
78+
return math.div(x, y);
5979
};
6080

6181
module.exports = engine;

src/equality.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,32 +38,35 @@ engine.unequival = function(a, b){
3838

3939
/**
4040
* Checks that the types of a and b are suitable for comparison in an
41-
* inequality expression. It is assumed that a check has already been made
42-
* that there is at least one value in a and b.
41+
* inequality expression.
4342
* @param a the left side of the inequality expression (which should be an array of
4443
* one value).
4544
* @param b the right side of the inequality expression (which should be an array of
4645
* one value).
4746
* @return the singleton values of the arrays a, and b. If one was an FP_Type
48-
* and the other was convertible, the coverted value will be retureed.
47+
* and the other was convertible, the converted value will be returned.
4948
*/
5049
function typecheck(a, b){
51-
util.assertAtMostOne(a, "Singleton was expected");
52-
util.assertAtMostOne(b, "Singleton was expected");
50+
util.assertOnlyOne(a, "Singleton was expected");
51+
util.assertOnlyOne(b, "Singleton was expected");
5352
a = util.valDataConverted(a[0]);
5453
b = util.valDataConverted(b[0]);
55-
let lClass = a instanceof FP_DateTime ? FP_DateTime : a.constructor;
56-
let rClass = b instanceof FP_DateTime ? FP_DateTime : b.constructor;
57-
if (lClass !== rClass) {
58-
util.raiseError('Type of "'+a+'" ('+lClass.name+') did not match type of "'+
59-
b+'" ('+rClass.name+')', 'InequalityExpression');
54+
if (a != null && b != null) {
55+
let lClass = a instanceof FP_DateTime ? FP_DateTime : a.constructor;
56+
let rClass = b instanceof FP_DateTime ? FP_DateTime : b.constructor;
57+
if (lClass !== rClass) {
58+
util.raiseError('Type of "' + a + '" (' + lClass.name + ') did not match type of "' +
59+
b + '" (' + rClass.name + ')', 'InequalityExpression');
60+
}
6061
}
6162
return [a, b];
6263
}
6364

6465
engine.lt = function(a, b){
65-
if (!a.length || !b.length) return [];
6666
const [a0, b0] = typecheck(a,b);
67+
if (a0 == null || b0 == null) {
68+
return [];
69+
}
6770
if (a0 instanceof FP_Type) {
6871
const compare = a0.compare(b0);
6972
return compare === null ? [] : compare < 0;
@@ -72,8 +75,10 @@ engine.lt = function(a, b){
7275
};
7376

7477
engine.gt = function(a, b){
75-
if (!a.length || !b.length) return [];
7678
const [a0, b0] = typecheck(a,b);
79+
if (a0 == null || b0 == null) {
80+
return [];
81+
}
7782
if (a0 instanceof FP_Type) {
7883
const compare = a0.compare(b0);
7984
return compare === null ? [] : compare > 0;
@@ -82,8 +87,10 @@ engine.gt = function(a, b){
8287
};
8388

8489
engine.lte = function(a, b){
85-
if (!a.length || !b.length) return [];
8690
const [a0, b0] = typecheck(a,b);
91+
if (a0 == null || b0 == null) {
92+
return [];
93+
}
8794
if (a0 instanceof FP_Type) {
8895
const compare = a0.compare(b0);
8996
return compare === null ? [] : compare <= 0;
@@ -92,8 +99,10 @@ engine.lte = function(a, b){
9299
};
93100

94101
engine.gte = function(a, b){
95-
if (!a.length || !b.length) return [];
96102
const [a0, b0] = typecheck(a,b);
103+
if (a0 == null || b0 == null) {
104+
return [];
105+
}
97106
if (a0 instanceof FP_Type) {
98107
const compare = a0.compare(b0);
99108
return compare === null ? [] : compare >= 0;

src/fhirpath.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ engine.invocationTable = {
7878
where: {fn: filtering.whereMacro, arity: {1: ["Expr"]}},
7979
extension: {fn: filtering.extension, arity: {1: ["String"]}},
8080
select: {fn: filtering.selectMacro, arity: {1: ["Expr"]}},
81-
aggregate: {fn: aggregate.aggregateMacro, arity: {1: ["Expr"], 2: ["Expr", "Any"]}},
81+
aggregate: {fn: aggregate.aggregateMacro, arity: {1: ["Expr"], 2: ["Expr", "AnyAtRoot"]}},
8282
sum: {fn: aggregate.sumFn},
8383
min: {fn: aggregate.minFn},
8484
max: {fn: aggregate.maxFn},
@@ -107,6 +107,7 @@ engine.invocationTable = {
107107
toTime: {fn: misc.toTime},
108108
toBoolean: {fn: misc.toBoolean},
109109
toQuantity: {fn: misc.toQuantity, arity: {0: [], 1: ["String"]}},
110+
// TODO: The hasValue function should be taken into account in a separate request
110111
hasValue: {fn: misc.hasValueFn},
111112
convertsToBoolean: {fn: misc.createConvertsToFn(misc.toBoolean, 'boolean')},
112113
convertsToInteger: {fn: misc.createConvertsToFn(misc.toInteger, 'number')},
@@ -143,7 +144,7 @@ engine.invocationTable = {
143144
ln: {fn: math.ln},
144145
log: {fn: math.log, arity: {1: ["Number"]}, nullable: true},
145146
power: {fn: math.power, arity: {1: ["Number"]}, nullable: true},
146-
round: {fn: math.round, arity: {1: ["Number"]}},
147+
round: {fn: math.round, arity: {0: [], 1: ["Number"]}},
147148
sqrt: {fn: math.sqrt},
148149
truncate: {fn: math.truncate},
149150

@@ -414,7 +415,7 @@ function makeParam(ctx, parentData, type, param) {
414415
return engine.TypeSpecifier(ctx, parentData, param);
415416
}
416417

417-
const res = engine.doEval(ctx, parentData, param);
418+
let res = engine.doEval(ctx, parentData, param);
418419
if(type === "Any") {
419420
return res;
420421
}
@@ -434,7 +435,7 @@ function doInvoke(ctx, fnName, data, rawParams){
434435
if(invoc) {
435436
if(!invoc.arity){
436437
if(!rawParams){
437-
res = invoc.fn.call(ctx, util.arraify(data));
438+
res = invoc.fn.call(ctx, data);
438439
return util.arraify(res);
439440
} else {
440441
throw new Error(fnName + " expects no params");

src/filtering.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ engine.singleFn = function(x) {
7272
} else if (x.length == 0) {
7373
return [];
7474
} else {
75-
//TODO: should throw error?
76-
return {$status: "error", $error: "Expected single"};
75+
throw new Error("Expected single");
7776
}
7877
};
7978

0 commit comments

Comments
 (0)