From 00662f855e61300ccd421c340664e78b7903d78a Mon Sep 17 00:00:00 2001 From: Paul Lynch Date: Wed, 29 Jun 2022 15:15:30 -0400 Subject: [PATCH 1/4] Fixed Quantity processing so that FHIR Quantities are not converted to System Quantities until necessary --- CHANGELOG.md | 8 +++- bin/fhirpath | 2 + package-lock.json | 2 +- package.json | 1 + src/deep-equal.js | 4 +- src/equality.js | 6 +-- src/hash-object.js | 4 +- src/math.js | 8 ++-- src/misc.js | 5 +-- src/types.js | 41 +++++++++++-------- src/utilities.js | 14 +++++++ test/api.test.js | 2 +- test/cases/fhir-quantity.yaml | 14 ++++++- test/fhirpath.test.js | 37 +++++++++-------- test/resources/quantity-example.json | 11 +++++ .../resources/questionnaire-part-example.json | 6 +-- 16 files changed, 110 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8793a8..b1fd74b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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. diff --git a/bin/fhirpath b/bin/fhirpath index 49fa35f..71150ae 100755 --- a/bin/fhirpath +++ b/bin/fhirpath @@ -50,5 +50,7 @@ else { let res = fp.evaluate(resource, base ? {base, expression} : expression, context, model); console.log('fhirpath(' + expression + ') =>'); console.log(JSON.stringify(res, null, " ")); + console.log(res.__path__); + console.log(res); } } diff --git a/package-lock.json b/package-lock.json index 20c9798..4a03e44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "2.14.5", + "version": "2.14.6", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 479f02e..a757dc5 100644 --- a/package.json +++ b/package.json @@ -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.\"", diff --git a/src/deep-equal.js b/src/deep-equal.js index a30726e..1ed5fb5 100644 --- a/src/deep-equal.js +++ b/src/deep-equal.js @@ -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 ===. diff --git a/src/equality.js b/src/equality.js index 8695ba5..fbd7104 100644 --- a/src/equality.js +++ b/src/equality.js @@ -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'); @@ -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) { diff --git a/src/hash-object.js b/src/hash-object.js index e2fe354..51c61b1 100644 --- a/src/hash-object.js +++ b/src/hash-object.js @@ -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'); /** @@ -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); diff --git a/src/math.js b/src/math.js index 96179f1..cdadf1b 100644 --- a/src/math.js +++ b/src/math.js @@ -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 @@ -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) diff --git a/src/misc.js b/src/misc.js index 2726e09..8e8d91e 100644 --- a/src/misc.js +++ b/src/misc.js @@ -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") { @@ -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(); }; diff --git a/src/types.js b/src/types.js index 01bcebf..e54546d 100644 --- a/src/types.js +++ b/src/types.js @@ -177,6 +177,7 @@ FP_Quantity.toUcumQuantity = function (value, unit) { }; }; + /** * Converts FHIRPath value/unit to other FHIRPath value/unit. * @param {string} fromUnit @@ -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, @@ -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 || {}; } @@ -1009,27 +1011,34 @@ 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 + * @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 = this.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 diff --git a/src/utilities.js b/src/utilities.js index 27d7ece..88d44b9 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -92,6 +92,20 @@ 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.convertData(); + val = val.data; + } + return val; +}; + /** * Prepares a string for insertion into a regular expression * @param {string} str diff --git a/test/api.test.js b/test/api.test.js index 53fc1c0..2f7f6a6 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -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); }) }); diff --git a/test/cases/fhir-quantity.yaml b/test/cases/fhir-quantity.yaml index 2dfd1cc..80fd8a3 100644 --- a/test/cases/fhir-quantity.yaml +++ b/test/cases/fhir-quantity.yaml @@ -65,4 +65,16 @@ tests: inputfile: quantity-example.json model: 'r4' expression: QuestionnaireResponse.item[2].answer.value.toQuantity() - result: [] \ No newline at end of file + 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: + - '>' + diff --git a/test/fhirpath.test.js b/test/fhirpath.test.js index 67bae4c..3f22640 100644 --- a/test/fhirpath.test.js +++ b/test/fhirpath.test.js @@ -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 => { diff --git a/test/resources/quantity-example.json b/test/resources/quantity-example.json index 6db21e6..8ec8fea 100644 --- a/test/resources/quantity-example.json +++ b/test/resources/quantity-example.json @@ -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" + } + } }] } diff --git a/test/resources/questionnaire-part-example.json b/test/resources/questionnaire-part-example.json index e984576..386adb9 100644 --- a/test/resources/questionnaire-part-example.json +++ b/test/resources/questionnaire-part-example.json @@ -1,10 +1,10 @@ { - "answer": { + "answer": [{ "valueQuantity": { "value": 2, "unit": "year(real UCUM code in field 'code')", "system": "http://unitsofmeasure.org", "code": "a" } - } -} \ No newline at end of file + }] +} From 5ac08a17022a6f6a12e6dc2db15cb57925b23f42 Mon Sep 17 00:00:00 2001 From: Paul Lynch Date: Fri, 22 Jul 2022 12:55:17 -0400 Subject: [PATCH 2/4] Updated version in package.json to 2.14.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a757dc5..57c9569 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fhirpath", - "version": "2.14.5", + "version": "2.14.6", "description": "A FHIRPath engine", "main": "src/fhirpath.js", "dependencies": { From 0e8a187ed5531555a7e87df681f35afd6b7b9e98 Mon Sep 17 00:00:00 2001 From: Paul Lynch Date: Fri, 22 Jul 2022 17:30:53 -0400 Subject: [PATCH 3/4] Changed ResourceNode.convertData to not modify this.data --- src/types.js | 3 ++- src/utilities.js | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types.js b/src/types.js index e54546d..84fcc06 100644 --- a/src/types.js +++ b/src/types.js @@ -1018,6 +1018,7 @@ class ResourceNode { * 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|...} @@ -1028,7 +1029,7 @@ class ResourceNode { 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 = this.data = + data = new FP_Quantity(data.value, FP_Quantity.mapUCUMCodeToTimeUnits[data.code] || '\'' + data.code + '\''); } } diff --git a/src/utilities.js b/src/utilities.js index 88d44b9..d14be7b 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -100,8 +100,7 @@ util.valData = function(val) { */ util.valDataConverted = function(val) { if (val instanceof ResourceNode) { - val.convertData(); - val = val.data; + val = val.convertData(); } return val; }; From 1b465d02a00c45d02b8d7558cc425d2a688ef7ac Mon Sep 17 00:00:00 2001 From: Paul Lynch Date: Thu, 4 Aug 2022 18:31:19 -0400 Subject: [PATCH 4/4] Removed lines that were for debugging --- bin/fhirpath | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/fhirpath b/bin/fhirpath index 71150ae..49fa35f 100755 --- a/bin/fhirpath +++ b/bin/fhirpath @@ -50,7 +50,5 @@ else { let res = fp.evaluate(resource, base ? {base, expression} : expression, context, model); console.log('fhirpath(' + expression + ') =>'); console.log(JSON.stringify(res, null, " ")); - console.log(res.__path__); - console.log(res); } }