Skip to content

Commit

Permalink
Merge pull request #53 from HL7/feature/LF-1450/implement-conversion-…
Browse files Browse the repository at this point in the history
…functions

Feature/lf 1450/implement conversion functions
  • Loading branch information
plynchnlm committed Jun 26, 2020
2 parents 01c7e18 + e496be1 commit d9fb9f5
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 89 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [2.3.0] - 2020-06-17
### Added
- Functions: toBoolean(), convertsToBoolean(), convertsToInteger(), convertsToDecimal(), convertsToString(),
convertsToDateTime(), convertsToTime(), convertsToQuantity()
### Fixed
- toInteger() function should return an empty collection for non-convertible string
- toQuantity() function should work with the entire input string, not part of it (RegExp expression surrounded with ^...$)
- toQuantity() function should support boolean values
- toQuantity() function should not accept a string where UCUM unit code is not surrounded with single quotes
- The third parameter of iif function should be optional

## [2.2.2] - 2020-06-05
### Fixed
- Updated FHIRPath test cases
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ fhirpath --expression 'Patient.name.given'

## Implementation Status

We are currently implementing version 1.0 (a.k.a STU1) of
[FHIRPath](http://hl7.org/fhirpath/).
We are currently working on implementing version 2.0.0 of
[FHIRPath](http://hl7.org/fhirpath/);
some behavior may still be following the previous version, STU1.

The core parser was generated from the FHIRPath ANTLR grammar.

Expand All @@ -143,7 +144,7 @@ Completed sections:
- 9 (Environment Variables)

Almost completed sections:
- 5.5 (Conversion) - unimplemented methods: toBoolean, toDate.
- 5.5 (Conversion) - unimplemented methods: toDate, convertsToDate.

We are deferring handling information about FHIR resources, as much as
possible, with the exception of support for choice types. This affects
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.2.2",
"version": "2.3.0",
"description": "A FHIRPath engine",
"main": "src/fhirpath.js",
"dependencies": {
Expand Down
10 changes: 9 additions & 1 deletion src/fhirpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,22 @@ engine.invocationTable = {
take: {fn: filtering.takeFn, arity: {1: ["Integer"]}},
skip: {fn: filtering.skipFn, arity: {1: ["Integer"]}},
combine: {fn: combining.combineFn, arity: {1: ["AnyAtRoot"]}},
iif: {fn: misc.iifMacro, arity: {3: ["Expr", "Expr", "Expr"]}},
iif: {fn: misc.iifMacro, arity: {2: ["Expr", "Expr"], 3: ["Expr", "Expr", "Expr"]}},
trace: {fn: misc.traceFn, arity: {0: [], 1: ["String"]}},
toInteger: {fn: misc.toInteger},
toDecimal: {fn: misc.toDecimal},
toString: {fn: misc.toString},
toDateTime: {fn: misc.toDateTime},
toTime: {fn: misc.toTime},
toBoolean: {fn: misc.toBoolean},
toQuantity: {fn: misc.toQuantity, arity: {0: [], 1: ["String"]}},
convertsToBoolean: {fn: misc.createConvertsToFn(misc.toBoolean, 'boolean')},
convertsToInteger: {fn: misc.createConvertsToFn(misc.toInteger, 'number')},
convertsToDecimal: {fn: misc.createConvertsToFn(misc.toDecimal, 'number')},
convertsToString: {fn: misc.createConvertsToFn(misc.toString, 'string')},
convertsToDateTime: {fn: misc.createConvertsToFn(misc.toDateTime, FP_DateTime)},
convertsToTime: {fn: misc.createConvertsToFn(misc.toTime, FP_Time)},
convertsToQuantity: {fn: misc.createConvertsToFn(misc.toQuantity, FP_Quantity)},

indexOf: {fn: strings.indexOf, arity: {1: ["String"]}},
substring: {fn: strings.substring, arity: {1: ["Integer"], 2: ["Integer","Integer"]}},
Expand Down
87 changes: 81 additions & 6 deletions src/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ engine.iifMacro = function(data, cond, ok, fail) {
if(util.isTrue(cond(data))) {
return ok(data);
} else {
return fail(data);
return fail ? fail(data) : [];
}
};

Expand All @@ -38,17 +38,19 @@ engine.toInteger = function(coll){
if(typeof v === "string") {
if(intRegex.test(v)){
return parseInt(v);
} else {
throw new Error("Could not convert to ineger: " + v);
}
}
return [];
};

const quantityRegex = /((\+|-)?\d+(\.\d+)?)\s*(('[^']+')|([a-zA-Z]+))?/,
const quantityRegex = /^((\+|-)?\d+(\.\d+)?)\s*(('[^']+')|([a-zA-Z]+))?$/,
quantityRegexMap = {value:1,unit:5,time:6};
engine.toQuantity = function (coll, toUnit) {
let result;
// Surround UCUM unit code in the toUnit parameter with single quotes
if (toUnit && !FP_Quantity.mapTimeUnitsToUCUMCode[toUnit]) {
toUnit = `'${toUnit}'`;
}

if (coll.length > 1) {
throw new Error("Could not convert to quantity: input collection contains multiple items");
Expand All @@ -61,12 +63,17 @@ engine.toQuantity = function (coll, toUnit) {
result = new FP_Quantity(v, '\'1\'');
} else if (v instanceof FP_Quantity) {
result = v;
} else if (typeof v === 'boolean') {
result = new FP_Quantity(v ? 1 : 0, '\'1\'');
} else if (typeof v === "string" && (quantityRegexRes = quantityRegex.exec(v)) ) {
const value = quantityRegexRes[quantityRegexMap.value],
unit = quantityRegexRes[quantityRegexMap.unit],
time = quantityRegexRes[quantityRegexMap.time];

result = new FP_Quantity(Number(value), unit||time||'\'1\'');
// UCUM unit code in the input string must be surrounded with single quotes
if (!time || FP_Quantity.mapTimeUnitsToUCUMCode[time]) {
result = new FP_Quantity(Number(value), unit || time || '\'1\'');
}
}

if (result && toUnit && result.unit !== toUnit) {
Expand Down Expand Up @@ -116,12 +123,80 @@ function defineTimeConverter(timeType) {
if (coll.length === 1) {
var t = types[timeType].checkString(util.valData(coll[0]));
if (t)
rtn[0] = t;
rtn = t;
}
return rtn;
};
}
defineTimeConverter('FP_DateTime');
defineTimeConverter('FP_Time');

// Possible string values convertible to the true boolean value
const trueStrings = ['true', 't', 'yes', 'y', '1', '1.0'].reduce((acc, val) => {
acc[val] = true;
return acc;
}, {});

// Possible string values convertible to the false boolean value
const falseStrings = ['false', 'f', 'no', 'n', '0', '0.0'].reduce((acc, val) => {
acc[val] = true;
return acc;
}, {});

engine.toBoolean = function (coll) {
if(coll.length !== 1) {
return [];
}

const v = util.valData(coll[0]);
switch (typeof v) {
case 'boolean':
return v;
case 'number':
if (v === 1) {
return true;
}
if (v === 0) {
return false;
}
break;
case 'string':
// eslint-disable-next-line no-case-declarations
const lowerCaseValue = v.toLowerCase();
if (trueStrings[lowerCaseValue]) {
return true;
}
if (falseStrings[lowerCaseValue]) {
return false;
}
}
return [];
};

/**
* Creates function that checks if toFunction returns specified type
* @param {function(coll: array): <type|[]>} toFunction
* @param {string|class} type - specifies type, for example: 'string' or FP_Quantity
* @return {function(coll: array)}
*/
engine.createConvertsToFn = function (toFunction, type) {
if (typeof type === 'string') {
return function (coll) {
if (coll.length !== 1) {
return [];
}

return typeof toFunction(coll) === type;
};
}

return function (coll) {
if (coll.length !== 1) {
return [];
}

return toFunction(coll) instanceof type;
};
};

module.exports = engine;
19 changes: 16 additions & 3 deletions test/cases/5.5_conversion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ tests:
expression: Functions.iif(coll1[0].coll2[0].attr = 1, coll1[0].coll2[0].attr, coll1[0].coll2[1].attr)
result: [1]

- desc: '** iif 4'
expression: Functions.iif(true, 'a')
result: ['a']

- desc: '** iif 5'
expression: Functions.iif(false, 'a')
result: []

- desc: '5.5.2. toInteger() : integer'
# If the input collection contains a single item, this functio# n will return a single integer if:
# the item in the input collection is an integer
Expand Down Expand Up @@ -137,7 +145,12 @@ tests:
- true
- desc: '** string to Quantity'
inputfile: patient-example.json
expression: '''1 \''wk\''''.toQuantity(''\''d\'''') = 7 days'
expression: "'1 \\'wk\\''.toQuantity('d') = 7 days"
result:
- true
- desc: '** string to Quantity - result UCUM unit code must be surrounded with single quotes'
inputfile: patient-example.json
expression: "'1 \\'wk\\''.toQuantity('d').toString() = '7 \\'d\\''"
result:
- true
- desc: '** calendar duration conversion factor'
Expand All @@ -152,12 +165,12 @@ tests:
- true
- desc: '** UCUM units'
inputfile: patient-example.json
expression: '''1 cm''.toQuantity(''\''mm\'''').value = 10'
expression: "'1 \\'cm\\''.toQuantity('mm').value = 10"
result:
- true
- desc: '** Invalid conversion'
inputfile: patient-example.json
expression: '''1 cm''.toQuantity(''\''g\'''')'
expression: "'1 \\'cm\\''.toQuantity('g')"
result: []
- desc: '** Implicit to quantity + toQuantity'
inputfile: observation-example.json
Expand Down
Loading

0 comments on commit d9fb9f5

Please sign in to comment.