Skip to content

Commit

Permalink
Merge pull request #43 in LFOR/fhirpath.js from LF-2345/add_missing_q…
Browse files Browse the repository at this point in the history
…uantity_fields to master

* commit '1b465d02a00c45d02b8d7558cc425d2a688ef7ac':
  Removed lines that were for debugging
  Changed ResourceNode.convertData to not modify this.data
  Updated version in package.json to 2.14.6
  Fixed Quantity processing so that FHIR Quantities are not converted to System Quantities until necessary
  • Loading branch information
plynchnlm committed Aug 5, 2022
2 parents a3a0445 + 1b465d0 commit 36c9f1e
Show file tree
Hide file tree
Showing 15 changed files with 109 additions and 56 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [2.14.6] - 2022-06-29
### Fixed
- FHIR Quantities are now only converted to System Quantities when necessary, so
that FHIR Quantities can be returned from an expression, and so that the
fields from a FHIR Quantity can be accessed.

## [2.14.5] - 2022-06-07
### Added
- Version number to fhirpath.js demo page.
Expand Down Expand Up @@ -91,7 +97,7 @@ This log documents significant changes for each release. This project follows
### Fixed
- String manipulation functions did not properly return an empty
collection when the input collection is empty.

## [2.7.4] - 2021-03-12
### Fixed
- Evaluation of singleton collections.
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.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fhirpath",
"version": "2.14.5",
"version": "2.14.6",
"description": "A FHIRPath engine",
"main": "src/fhirpath.js",
"dependencies": {
Expand Down Expand Up @@ -45,6 +45,7 @@
"generateParser": "cd src/parser; rimraf ./generated/*; java -Xmx500M -cp \"../../antlr-4.9.3-complete.jar:$CLASSPATH\" org.antlr.v4.Tool -o generated -Dlanguage=JavaScript FHIRPath.g4; grunt updateParserRequirements",
"build": "cd browser-build && webpack && rimraf fhirpath.zip && bestzip fhirpath.zip LICENSE.md fhirpath.min.js fhirpath.r4.min.js fhirpath.stu3.min.js fhirpath.dstu2.min.js && rimraf LICENSE.md",
"test:unit": "jest && TZ=America/New_York jest && TZ=Europe/Paris jest",
"test:unit:debug": "echo 'open chrome chrome://inspect/' && node --inspect node_modules/.bin/jest --runInBand",
"test:demo": "npm run build && cd demo && npm run build && grunt test:e2e",
"test:build": "cd browser-build && grunt test:e2e",
"test": "npm run lint && npm run test:unit && npm run update-webdriver && npm run test:demo && npm run test:build && echo \"For tests specific to IE 11, open browser-build/test/protractor/index.html in IE 11, and confirm that the tests on that page pass.\"",
Expand Down
4 changes: 2 additions & 2 deletions src/deep-equal.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ function normalizeStr(x) {
* @return {boolean}
*/
function deepEqual(actual, expected, opts) {
actual = util.valData(actual);
expected = util.valData(expected);
actual = util.valDataConverted(actual);
expected = util.valDataConverted(expected);
if (!opts) opts = {};

// 7.1. All identical values are equivalent, as determined by ===.
Expand Down
6 changes: 3 additions & 3 deletions src/equality.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file holds code to hande the FHIRPath Math functions.
// This file holds code to handle the FHIRPath Math functions.

var util = require("./utilities");
var deepEqual = require('./deep-equal');
Expand Down Expand Up @@ -52,8 +52,8 @@ function typecheck(a, b){
let rtn = null;
util.assertAtMostOne(a, "Singleton was expected");
util.assertAtMostOne(b, "Singleton was expected");
a = util.valData(a[0]);
b = util.valData(b[0]);
a = util.valDataConverted(a[0]);
b = util.valDataConverted(b[0]);
let lClass = a.constructor;
let rClass = b.constructor;
if (lClass != rClass) {
Expand Down
4 changes: 2 additions & 2 deletions src/hash-object.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const ucumUtils = require('@lhncbc/ucum-lhc').UcumLhcUtils.getInstance();
const {roundToMaxPrecision} = require('./numbers');
const {valData} = require('./utilities');
const {valDataConverted} = require('./utilities');
const {FP_Type, FP_Quantity} = require('./types');

/**
Expand All @@ -23,7 +23,7 @@ function hashObject(obj) {
* here they are likely also needed there).
*/
function prepareObject(value) {
value = valData(value);
value = valDataConverted(value);

if (typeof value === 'number') {
return roundToMaxPrecision(value);
Expand Down
8 changes: 4 additions & 4 deletions src/math.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ engine.amp = function(x, y){
// Actually, "minus" is now also polymorphic
engine.plus = function(xs, ys){
if(xs.length == 1 && ys.length == 1) {
var x = util.valData(xs[0]);
var y = util.valData(ys[0]);
var x = util.valDataConverted(xs[0]);
var y = util.valDataConverted(ys[0]);
// In the future, this and other functions might need to return ResourceNode
// to preserve the type information (integer vs decimal, and maybe decimal
// vs string if decimals are represented as strings), in order to support
Expand All @@ -59,8 +59,8 @@ engine.plus = function(xs, ys){

engine.minus = function(xs, ys){
if(xs.length == 1 && ys.length == 1) {
var x = util.valData(xs[0]);
var y = util.valData(ys[0]);
var x = util.valDataConverted(xs[0]);
var y = util.valDataConverted(ys[0]);
if(typeof x == "number" && typeof y == "number")
return x - y;
if(x instanceof FP_TimeBase && y instanceof FP_Quantity)
Expand Down
5 changes: 2 additions & 3 deletions src/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ engine.toQuantity = function (coll, toUnit) {
if (coll.length > 1) {
throw new Error("Could not convert to quantity: input collection contains multiple items");
} else if (coll.length === 1) {
const item = coll[0],
v = util.valData(item);
var v = util.valDataConverted(coll[0]);
let quantityRegexRes;

if (typeof v === "number") {
Expand Down Expand Up @@ -99,7 +98,7 @@ engine.toDecimal = function(coll){

engine.toString = function(coll){
if(coll.length !== 1) { return []; }
var v = util.valData(coll[0]);
var v = util.valDataConverted(coll[0]);
return v.toString();
};

Expand Down
42 changes: 26 additions & 16 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ FP_Quantity.toUcumQuantity = function (value, unit) {
};
};


/**
* Converts FHIRPath value/unit to other FHIRPath value/unit.
* @param {string} fromUnit
Expand Down Expand Up @@ -219,6 +220,7 @@ FP_Quantity.convUnitTo = function (fromUnit, value, toUnit) {
return null;
};


// Defines conversion factors for calendar durations
FP_Quantity._calendarDuration2Seconds = {
'years': 365*24*60*60,
Expand Down Expand Up @@ -987,7 +989,7 @@ class ResourceNode {
if (data?.resourceType)
path = data.resourceType;
this.path = path;
this.data = getResourceNodeData(data, path);
this.data = data;
this._data = _data || {};
}

Expand All @@ -1009,27 +1011,35 @@ class ResourceNode {
toJSON() {
return JSON.stringify(this.data);
}
}

/**
* Prepare data for ResourceNode:
* Converts value from FHIR Quantity to FHIRPath System.Quantity.
* The Mapping from FHIR Quantity to FHIRPath System.Quantity is explained here:
* https://www.hl7.org/fhir/fhirpath.html#quantity
* @param {Object|...} data
* @param {string} path
* @return {FP_Quantity|Object|...}
*/
function getResourceNodeData(data, path) {
if (path === 'Quantity' && data?.system === ucumSystemUrl) {
if (typeof data.value === 'number' && typeof data.code === 'string') {
data = new FP_Quantity(data.value, FP_Quantity.mapUCUMCodeToTimeUnits[data.code] || '\'' + data.code + '\'');
/**
* Converts the data value from FHIR a Quantity to FHIRPath System.Quantity,
* when possible, or if not returns the data as is. Throws an exception if
* the data is a Quantity that has a comparator.
* The Mapping from FHIR Quantity to FHIRPath System.Quantity is explained here:
* https://www.hl7.org/fhir/fhirpath.html#quantity
* this.data is not changed, but converted value is returned.
* @param {Object|...} data
* @param {string} path
* @return {FP_Quantity|Object|...}
*/
convertData() {
var data = this.data;
if (this.path === 'Quantity' && data?.system === ucumSystemUrl) {
if (typeof data.value === 'number' && typeof data.code === 'string') {
if (data.comparator !== undefined)
throw new Error('Cannot convert a FHIR.Quantity that has a comparator');
data =
new FP_Quantity(data.value, FP_Quantity.mapUCUMCodeToTimeUnits[data.code] || '\'' + data.code + '\'');
}
}

return data;
}

return data;
}


/**
* Returns a ResourceNode for the given data node, checking first to see if the
* given node is already a ResourceNode. Takes the same arguments as the
Expand Down
13 changes: 13 additions & 0 deletions src/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ util.valData = function(val) {
return (val instanceof ResourceNode) ? val.data : val;
};

/**
* Returns the data value of the given parameter, which might be a ResourceNode.
* Otherwise, it returns the value that was passed in. In the case of a
* ResourceNode that is a Quantity, the returned value will have been converted
* to an FP_Quantity.
*/
util.valDataConverted = function(val) {
if (val instanceof ResourceNode) {
val = val.convertData();
}
return val;
};

/**
* Prepares a string for insertion into a regular expression
* @param {string} str
Expand Down
2 changes: 1 addition & 1 deletion test/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('evaluate', () => {
{someVar},
r4_model
);
expect(result).toEqual(['1', '2', '3']);
expect(result).toEqual(['1', '2', '3', '4']);
expect(someVar).toStrictEqual(someVarOrig);
})
});
14 changes: 13 additions & 1 deletion test/cases/fhir-quantity.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,16 @@ tests:
inputfile: quantity-example.json
model: 'r4'
expression: QuestionnaireResponse.item[2].answer.value.toQuantity()
result: []
result: []
- desc: Error when a comparator is present and there is a need to convert
inputfile: quantity-example.json
model: 'r4'
expression: QuestionnaireResponse.item[3].answer.value.toQuantity()
error: true
- desc: Can access the comparator field when there isn't a need to convert
inputfile: quantity-example.json
model: 'r4'
expression: QuestionnaireResponse.item[3].answer.value.comparator
result:
- '>'

37 changes: 19 additions & 18 deletions test/fhirpath.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,33 @@ const generateTest = (test, testResource) => {
if (test.disableConsoleLog) {
console.log = function() {};
}
if (!test.error && test.expression) {
const result = calcExpression(expression, test, testResource);
// Run the result through JSON so the FP_Type quantities get converted to
// strings. Also , if the result is an FP_DateTime, convert to a Date
// object so that timezone differences are handled.
if (result.length == 1 && result[0] instanceof FP_DateTime)
expect(new Date(result[0])).toEqual(new Date(test.result[0]))
else
expect(JSON.parse(JSON.stringify(result))).toEqual(test.result);
}
else if (test.error) {
if (test.expression) { // Headings do not have expressions
let exception = null;
let result = null;
try {
result = fhirpath.evaluate(testResource, expression, null,
getFHIRModel(test.model));
result = calcExpression(expression, test, testResource);
}
catch (error) {
exception = error;
}
if (result != null)
console.log(result);
expect(exception).not.toBe(null);
if (!test.error) {
// Run the result through JSON so the FP_Type quantities get converted to
// strings. Also , if the result is an FP_DateTime, convert to a Date
// object so that timezone differences are handled.
if (result.length == 1 && result[0] instanceof FP_DateTime)
expect(new Date(result[0])).toEqual(new Date(test.result[0]))
else
expect(JSON.parse(JSON.stringify(result))).toEqual(test.result);
expect(exception).toBe(null);
}
else if (test.error) {
if (result != null)
console.log(result);
expect(exception).not.toBe(null);
}
if (test.disableConsoleLog)
console.log = console_log;
}
if (test.disableConsoleLog)
console.log = console_log;
};

expressions.forEach(expression => {
Expand Down
11 changes: 11 additions & 0 deletions test/resources/quantity-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,16 @@
"code": "min"
}
}
},{
"linkId": "4",
"answer": {
"valueQuantity": {
"value": 1,
"comparator": ">",
"unit": "year(real UCUM code in field 'code')",
"system": "http://unitsofmeasure.org",
"code": "min"
}
}
}]
}
6 changes: 3 additions & 3 deletions test/resources/questionnaire-part-example.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"answer": {
"answer": [{
"valueQuantity": {
"value": 2,
"unit": "year(real UCUM code in field 'code')",
"system": "http://unitsofmeasure.org",
"code": "a"
}
}
}
}]
}

0 comments on commit 36c9f1e

Please sign in to comment.