Skip to content

Commit

Permalink
Merge pull request #39 from HL7/feature/LF-1351/method-to-quantity
Browse files Browse the repository at this point in the history
Implement toQuantity(unit)
  • Loading branch information
plynchnlm committed Apr 6, 2020
2 parents aeccff6 + 18511d7 commit f049e4c
Show file tree
Hide file tree
Showing 11 changed files with 986 additions and 457 deletions.
6 changes: 6 additions & 0 deletions 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.1.0] - 2020-03-26
### Added
- Function toQuantity(unit)
- Operators =(equality) and ~(equivalence) for Quantity
- Implicit conversion from FHIR Quantity to FHIRPath System.Quantity

## [2.0.0] - 2020-03-11
### Changed
- FHIRPath grammar updated to version 2.0.0 (N1)
Expand Down
880 changes: 489 additions & 391 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fhirpath",
"version": "2.0.0",
"version": "2.1.0",
"description": "A FHIRPath engine",
"main": "src/fhirpath.js",
"dependencies": {
Expand All @@ -19,7 +19,7 @@
"bestzip": "^2.1.4",
"copy-webpack-plugin": "^5.1.1",
"eslint": "^5.2.0",
"grunt": "^1.0.4",
"grunt": "^1.1.0",
"grunt-cli": "^1.3.1",
"grunt-contrib-connect": "^2.0.0",
"grunt-protractor-runner": "^5.0.0",
Expand Down
66 changes: 3 additions & 63 deletions src/deep-equal.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
var types = require('./types');
var FP_Type = types.FP_Type;
var util = require('./utilities');
var numbers = require('./numbers');
var pSlice = Array.prototype.slice;
var objectKeys = Object.keys;
var isArguments = function (object) {
Expand All @@ -24,54 +25,6 @@ function normalizeStr(x) {
}


// Returns the number of digits in the number after the decimal point, ignoring
// trailing zeros.
function decimalPlaces(x) {
// Based on https://stackoverflow.com/a/9539746/360782
// Make sure it is a number and use the builtin number -> string.
var s = "" + (+x);
var match = /(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(s);
// NaN or Infinity or integer.
// We arbitrarily decide that Infinity is integral.
if (!match) { return 0; }
// Count the number of digits in the fraction and subtract the
// exponent to simulate moving the decimal point left by exponent places.
// 1.234e+2 has 1 fraction digit and '234'.length - 2 == 1
// 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5
//var wholeNum = match[1];
var fraction = match[2];
var exponent = match[3];
return Math.max(
0, // lower limit.
(fraction == '0' ? 0 : (fraction || '').length) // fraction length
- (exponent || 0)); // exponent
}


/**
* The smallest representable number in FHIRPath.
*/
const PRECISION_STEP = 1e-8;

/**
* Rounds a number to the nearest multiple of PRECISION_STEP.
*/
function roundToMaxPrecision(x) {
return Math.round(x/PRECISION_STEP)*PRECISION_STEP;
}


/**
* Rounds a number to the specified number of decimal places.
* @param x the decimal number to be rounded
* @param n the (maximum) number of decimal places to preserve. (The result
* could contain fewer if the decimal digits in x contain zeros).
*/
function roundToDecimalPlaces(x, n) {
var scale = Math.pow(10, n);
return Math.round(x*scale)/scale;
}

var deepEqual = function (actual, expected, opts) {
actual = util.valData(actual);
expected = util.valData(expected);
Expand All @@ -86,29 +39,16 @@ var deepEqual = function (actual, expected, opts) {
if(isString(actual) && isString(expected)) {
return normalizeStr(actual) == normalizeStr(expected);
}

if(Number.isInteger(actual) && Number.isInteger(expected)) {
return actual === expected;
}

if(isNumber(actual) && isNumber(expected)) {
var prec = Math.min(decimalPlaces(actual), decimalPlaces(expected));
if(prec === 0){
return Math.round(actual) === Math.round(expected);
} else {
// Note: Number.parseFloat(0.00000011).toPrecision(7) === "1.100000e-7"
// It does # of significant digits, not decimal places.
return roundToDecimalPlaces(actual, prec) ===
roundToDecimalPlaces(expected, prec);
}
return numbers.isEquivalent(actual, expected);
}
}
else { // !opts.fuzzy
// If these are numbers, they need to be rounded to the maximum supported
// precision to remove floating point arithmetic errors (e.g. 0.1+0.1+0.1 should
// equal 0.3) before comparing.
if (typeof actual === 'number' && typeof expected === 'number') {
return roundToMaxPrecision(actual) === roundToMaxPrecision(expected);
return numbers.isEqual(actual, expected);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/fhirpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ engine.invocationTable = {
toString: {fn: misc.toString},
toDateTime: {fn: misc.toDateTime},
toTime: {fn: misc.toTime},
toQuantity: {fn: misc.toQuantity, arity: {0: [], 1: ["String"]}},

indexOf: {fn: strings.indexOf, arity: {1: ["String"]}},
substring: {fn: strings.substring, arity: {1: ["Integer"], 2: ["Integer","Integer"]}},
Expand Down
30 changes: 30 additions & 0 deletions src/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
var util = require("./utilities");
var types = require("./types");

const { FP_Quantity } = types;

var engine = {};

engine.iifMacro = function(data, cond, ok, fail) {
Expand Down Expand Up @@ -43,6 +45,34 @@ engine.toInteger = function(coll){
return [];
};

const quantityRegex = /(?<value>(\+|-)?\d+(\.\d+)?)\s*((?<unit>'[^']+')|(?<time>[a-zA-Z]+))?/;
engine.toQuantity = function (coll, toUnit) {
let result;

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);
let quantityRegexRes;

if (typeof v === "number") {
result = new FP_Quantity(v, '\'1\'');
} else if (v instanceof FP_Quantity) {
result = v;
} else if (typeof v === "string" && (quantityRegexRes = quantityRegex.exec(v)) ) {
let {groups: {value, unit, time}} = quantityRegexRes;
result = new FP_Quantity(Number(value), unit||time||'\'1\'');
}

if (result && toUnit && result.unit !== toUnit) {
result = FP_Quantity.convUnitTo(result.unit, result.value, toUnit);
}
}

return result || [];
};

var numRegex = /^[+-]?\d+(\.\d+)?$/;
engine.toDecimal = function(coll){
if(coll.length != 1) { return []; }
Expand Down
82 changes: 82 additions & 0 deletions src/numbers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
let numberFns = {};

// Returns the number of digits in the number after the decimal point, ignoring
// trailing zeros.
function decimalPlaces(x) {
// Based on https://stackoverflow.com/a/9539746/360782
// Make sure it is a number and use the builtin number -> string.
const s = "" + (+x),
match = /(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(s);
// NaN or Infinity or integer.
// We arbitrarily decide that Infinity is integral.
if (!match) { return 0; }
// Count the number of digits in the fraction and subtract the
// exponent to simulate moving the decimal point left by exponent places.
// 1.234e+2 has 1 fraction digit and '234'.length - 2 == 1
// 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5
//var wholeNum = match[1];
const fraction = match[2],
exponent = match[3];
return Math.max(
0, // lower limit.
(fraction === '0' ? 0 : (fraction || '').length) // fraction length
- (exponent || 0)); // exponent
}

/**
* Rounds a number to the specified number of decimal places.
* @param x the decimal number to be rounded
* @param n the (maximum) number of decimal places to preserve. (The result
* could contain fewer if the decimal digits in x contain zeros).
*/
function roundToDecimalPlaces (x, n) {
const scale = Math.pow(10, n);
return Math.round(x*scale)/scale;
}

/**
* The smallest representable number in FHIRPath.
*/
const PRECISION_STEP = 1e-8;

/**
* Rounds a number to the nearest multiple of PRECISION_STEP.
*/
function roundToMaxPrecision(x) {
return Math.round(x/PRECISION_STEP)*PRECISION_STEP;
}

/**
* Determines numbers equivalence
* @param {number} actual
* @param {number} expected
* @return {boolean}
*/
numberFns.isEquivalent = function(actual, expected) {
if(Number.isInteger(actual) && Number.isInteger(expected)) {
return actual === expected;
}

const prec = Math.min(decimalPlaces(actual), decimalPlaces(expected));

if(prec === 0){
return Math.round(actual) === Math.round(expected);
} else {
// Note: Number.parseFloat(0.00000011).toPrecision(7) === "1.100000e-7"
// It does # of significant digits, not decimal places.
return roundToDecimalPlaces(actual, prec) ===
roundToDecimalPlaces(expected, prec);
}
};

/**
* Determines numbers equality
* @param {number} actual
* @param {number} expected
* @return {boolean}
*/
numberFns.isEqual = function(actual, expected) {
return roundToMaxPrecision(actual) === roundToMaxPrecision(expected);
};

module.exports = numberFns;
Loading

0 comments on commit f049e4c

Please sign in to comment.