quality-measure-engine 0.2.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +34 -6
- data/Rakefile +2 -0
- data/VERSION +1 -1
- data/js/{map-reduce-utils.js → map_reduce_utils.js} +50 -85
- data/js/{underscore-min.js → underscore_min.js} +3 -2
- data/lib/qme/database_access.rb +30 -0
- data/lib/qme/importer/code_system_helper.rb +3 -1
- data/lib/qme/importer/entry.rb +12 -22
- data/lib/qme/importer/generic_importer.rb +89 -30
- data/lib/qme/importer/patient_importer.rb +37 -21
- data/lib/qme/importer/property_matcher.rb +7 -2
- data/lib/qme/importer/section_importer.rb +31 -9
- data/lib/qme/map/map_reduce_builder.rb +14 -12
- data/lib/qme/map/map_reduce_executor.rb +34 -98
- data/lib/qme/map/measure_calculation_job.rb +33 -0
- data/lib/qme/measure/database_loader.rb +18 -25
- data/lib/qme/measure/measure_loader.rb +70 -37
- data/lib/qme/measure/properties_builder.rb +184 -0
- data/lib/qme/measure/properties_converter.rb +27 -0
- data/lib/qme/quality_measure.rb +41 -0
- data/lib/qme/quality_report.rb +61 -0
- data/lib/quality-measure-engine.rb +19 -16
- data/lib/tasks/fixtures.rake +91 -0
- data/lib/tasks/measure.rake +55 -36
- data/lib/tasks/mongo.rake +13 -19
- data/lib/tasks/patient_random.rake +2 -3
- metadata +174 -102
- data/lib/qme/mongo_helpers.rb +0 -15
data/README.md
CHANGED
@@ -1,7 +1,22 @@
|
|
1
|
-
This project
|
1
|
+
This project is a library designed to calculate clinical quality measures over a given population. Quality measures are described via JSON and provide the details on what information is needed from a patient record to calculate a quality measure. The logic of the measure is described in JavaScript using the MapReduce algorithmic framework.
|
2
|
+
|
3
|
+
Usage
|
4
|
+
=====
|
5
|
+
|
6
|
+
Extracting Measure Data from a HITSP C32
|
7
|
+
----------------------------------------
|
8
|
+
|
9
|
+
Each quality measure will need to extract specific information from a HITSP C32 for calculation. First, for each quality measure, a QME::Importer::GenericImporter should be created by passing in the JSON definition of the quality measure.
|
10
|
+
|
11
|
+
Next, an instance of QME::Importer::PatientImporter should be obtained by calling instance (it follows the singleton pattern). Add the GenericImporters for each desired measure with the add measure method. Finally, you can get a JSON representation of a patient record with the necessary information extracted by calling parse_c32.
|
12
|
+
|
13
|
+
Calculating Quality Measures
|
14
|
+
----------------------------
|
15
|
+
|
16
|
+
Results of quality measures are represented by QME::QualityReport. This class provides ways to determine if a report has been calculated for a population in the database as well as ways to create jobs to run the calculations.
|
2
17
|
|
3
18
|
Environment
|
4
|
-
|
19
|
+
===========
|
5
20
|
|
6
21
|
This project currently uses Ruby 1.9.2 and is built using [Bundler](http://gembundler.com/). To get all of the dependencies for the project, first install bundler:
|
7
22
|
|
@@ -11,12 +26,25 @@ Then run bundler to grab all of the necessary gems:
|
|
11
26
|
|
12
27
|
bundle install
|
13
28
|
|
14
|
-
The Quality Measure engine relies on a MongoDB [MongoDB](http://www.mongodb.org/) running a minimum of version 1.
|
29
|
+
The Quality Measure engine relies on a MongoDB [MongoDB](http://www.mongodb.org/) running a minimum of version 1.8.* or higher. To get and install Mongo refer to:
|
30
|
+
|
31
|
+
http://www.mongodb.org/display/DOCS/Quickstart
|
15
32
|
|
16
|
-
|
33
|
+
It also relies on [Redis](http://redis.io/) for background jobs via [Resque](https://github.com/defunkt/resque). To install Redis, please refer to:
|
34
|
+
|
35
|
+
http://redis.io/download
|
36
|
+
|
37
|
+
You can also find information on Redis at the [Resque homepage](https://github.com/defunkt/resque). Resque is used by this project to calculate quality measures in background jobs. We also use [resque-status](https://github.com/quirkey/resque-status). Please consult the resque-status instructions for working with the resque-web application if you would like to use it to monitor status.
|
38
|
+
|
39
|
+
Running Resque Workers
|
40
|
+
----------------------
|
41
|
+
|
42
|
+
QME::QualityReport will kick off background jobs with Resque. For these jobs to to actually get performed, you need to be running resque workers. This can be done with the following:
|
43
|
+
|
44
|
+
QUEUE=* bundle exec rake resque:work
|
17
45
|
|
18
46
|
Testing
|
19
|
-
|
47
|
+
=======
|
20
48
|
|
21
49
|
This project uses [RSpec](http://github.com/rspec/rspec-core) for testing. To run the suite, just enter the following:
|
22
50
|
|
@@ -56,6 +84,6 @@ This project uses [metric_fu](http://metric-fu.rubyforge.org/) for source code a
|
|
56
84
|
The project is currently configured to run Flay, Flog, Churn, Reek and Roodi
|
57
85
|
|
58
86
|
Project Practices
|
59
|
-
|
87
|
+
=================
|
60
88
|
|
61
89
|
Please try to follow our [Coding Style Guides](http://github.com/eedrummer/styleguide). Additionally, we will be using git in a pattern similar to [Vincent Driessen's workflow](http://nvie.com/posts/a-successful-git-branching-model/). While feature branches are encouraged, they are not required to work on the project.
|
data/Rakefile
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
require 'rspec/core/rake_task'
|
2
2
|
require 'yard'
|
3
3
|
require 'metric_fu'
|
4
|
+
require 'resque/tasks'
|
4
5
|
|
5
6
|
ENV['MEASURE_DIR'] = ENV['MEASURE_DIR'] || File.join('fixtures', 'measure_defs')
|
7
|
+
ENV['MEASURE_PROPS'] = ENV['MEASURE_PROPS'] || File.join('fixtures', 'measure_props')
|
6
8
|
|
7
9
|
Dir['lib/tasks/*.rake'].sort.each do |ext|
|
8
10
|
load ext
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.0
|
@@ -1,16 +1,24 @@
|
|
1
|
+
function() {
|
1
2
|
// Adds common utility functions to the root JS object. These are then
|
2
3
|
// available for use by the map-reduce functions for each measure.
|
3
4
|
// lib/qme/mongo_helpers.rb executes this function on a database
|
4
5
|
// connection.
|
5
|
-
(function() {
|
6
6
|
|
7
7
|
var root = this;
|
8
8
|
|
9
|
+
// Takes an arbitrary number of arrays and single values and returns a flattened
|
10
|
+
// array of all of the elements with any null values removed. For efficiency, if
|
11
|
+
// called with just a single array then it simply returns that array
|
12
|
+
root.normalize = function() {
|
13
|
+
if (arguments.length==1 && _.isArray(arguments[0]))
|
14
|
+
return arguments[0];
|
15
|
+
return _.compact(_.flatten(arguments));
|
16
|
+
}
|
17
|
+
|
9
18
|
// returns the number of values which fall between the supplied limits
|
10
19
|
// value may be a number or an array of numbers
|
11
20
|
root.inRange = function(value, min, max) {
|
12
|
-
|
13
|
-
value = [value];
|
21
|
+
value = normalize(value);
|
14
22
|
var count = 0;
|
15
23
|
for (i=0;i<value.length;i++) {
|
16
24
|
if ((value[i]>=min) && (value[i]<=max))
|
@@ -21,8 +29,7 @@
|
|
21
29
|
|
22
30
|
// returns the largest member of value that is within the supplied range
|
23
31
|
root.maxInRange = function(value, min, max) {
|
24
|
-
|
25
|
-
return null;
|
32
|
+
value = normalize(value);
|
26
33
|
var allInRange = _.select(value, function(v) {return v>=min && v<=max;});
|
27
34
|
return _.max(allInRange);
|
28
35
|
}
|
@@ -30,77 +37,63 @@
|
|
30
37
|
// returns the number of values which are less than the supplied limit
|
31
38
|
// value may be a number or an array of numbers
|
32
39
|
root.lessThan = function(value, max) {
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
for (i=0;i<value.length;i++) {
|
37
|
-
if (value[i]<=max)
|
38
|
-
count++;
|
39
|
-
}
|
40
|
-
return count;
|
40
|
+
value = normalize(value);
|
41
|
+
var matching = _.select(value, function(v) {return v<=max;});
|
42
|
+
return matching.length;
|
41
43
|
};
|
42
44
|
|
43
|
-
// Returns
|
44
|
-
//
|
45
|
-
// function always returns false
|
45
|
+
// Returns true if any conditions[i].end is within the specified period,
|
46
|
+
// false otherwise
|
46
47
|
root.conditionResolved = function(conditions, startDate, endDate) {
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
});
|
51
|
-
} else {
|
52
|
-
return false
|
48
|
+
conditions = normalize(conditions);
|
49
|
+
var resolvedWithinPeriod = function(condition) {
|
50
|
+
return (condition.end>=startDate && condition.end<=endDate);
|
53
51
|
};
|
52
|
+
return _.any(conditions, resolvedWithinPeriod);
|
54
53
|
};
|
55
54
|
|
56
55
|
// Returns the minimum of readings[i].value where readings[i].date is in
|
57
56
|
// the supplied startDate and endDate. If no reading meet this criteria,
|
58
57
|
// returns defaultValue.
|
59
58
|
root.minValueInDateRange = function(readings, startDate, endDate, defaultValue) {
|
59
|
+
readings = normalize(readings);
|
60
|
+
|
60
61
|
var readingInDateRange = function(reading) {
|
61
|
-
|
62
|
-
return result;
|
62
|
+
return (reading.date>=startDate && reading.date<=endDate);
|
63
63
|
};
|
64
64
|
|
65
|
-
if (!readings || readings.length<1)
|
66
|
-
return defaultValue;
|
67
|
-
|
68
65
|
var allInDateRange = _.select(readings, readingInDateRange);
|
69
|
-
|
70
|
-
if (min)
|
71
|
-
return min.value;
|
72
|
-
else
|
66
|
+
if (allInDateRange.length==0)
|
73
67
|
return defaultValue;
|
68
|
+
var min = _.min(allInDateRange, function(reading) {return reading.value;});
|
69
|
+
return min.value;
|
74
70
|
};
|
75
71
|
|
76
72
|
// Returns the most recent readings[i].value where readings[i].date is in
|
77
73
|
// the supplied startDate and endDate. If no reading meet this criteria,
|
78
74
|
// returns defaultValue.
|
79
75
|
root.latestValueInDateRange = function(readings, startDate, endDate, defaultValue) {
|
76
|
+
readings = normalize(readings);
|
77
|
+
|
80
78
|
var readingInDateRange = function(reading) {
|
81
|
-
|
82
|
-
return result;
|
79
|
+
return (reading.date>=startDate && reading.date<=endDate);
|
83
80
|
};
|
84
81
|
|
85
|
-
if (!readings || readings.length<1)
|
86
|
-
return defaultValue;
|
87
|
-
|
88
82
|
var allInDateRange = _.select(readings, readingInDateRange);
|
89
|
-
|
90
|
-
if (latest)
|
91
|
-
return latest.value;
|
92
|
-
else
|
83
|
+
if (allInDateRange.length==0)
|
93
84
|
return defaultValue;
|
85
|
+
var latest = _.max(allInDateRange, function(reading) {return reading.date;});
|
86
|
+
return latest.value;
|
94
87
|
};
|
95
88
|
|
96
89
|
// Returns the number of actions that occur within the specified time period of
|
97
90
|
// something. The first two arguments are arrays or single-valued time stamps in
|
98
91
|
// seconds-since-the-epoch, timePeriod is in seconds.
|
99
92
|
root.actionFollowingSomething = function(something, action, timePeriod) {
|
100
|
-
|
101
|
-
|
102
|
-
if (
|
103
|
-
|
93
|
+
something = normalize(something);
|
94
|
+
action = normalize(action);
|
95
|
+
if (timePeriod===undefined)
|
96
|
+
timePeriod=Infinity;
|
104
97
|
|
105
98
|
var result = 0;
|
106
99
|
for (i=0; i<something.length; i++) {
|
@@ -114,61 +107,33 @@
|
|
114
107
|
return result;
|
115
108
|
}
|
116
109
|
|
117
|
-
// Returns the number of actions that occur after
|
118
|
-
// something. The first two arguments are arrays or single-valued time stamps in
|
119
|
-
// seconds-since-the-epoch.
|
120
|
-
root.actionAfterSomething = function(something, action) {
|
121
|
-
if (!_.isArray(something))
|
122
|
-
something = [something];
|
123
|
-
if (!_.isArray(action))
|
124
|
-
action = [action];
|
125
|
-
|
126
|
-
var result = 0;
|
127
|
-
for (i=0; i<something.length; i++) {
|
128
|
-
var timeStamp = something[i];
|
129
|
-
for (j=0; j<action.length;j++) {
|
130
|
-
if (action[j]>=timeStamp )
|
131
|
-
result++;
|
132
|
-
}
|
133
|
-
}
|
134
|
-
return result;
|
135
|
-
}
|
136
|
-
|
137
110
|
// Returns all members of the values array that fall between min and max inclusive
|
138
111
|
root.selectWithinRange = function(values, min, max) {
|
112
|
+
values = normalize(values);
|
139
113
|
return _.select(values, function(value) { return value<=max && value>=min; });
|
140
114
|
}
|
141
115
|
|
142
116
|
root.map = function(record, population, denominator, numerator, exclusion) {
|
143
|
-
var value = {population:
|
117
|
+
var value = {population: false, denominator: false, numerator: false,
|
118
|
+
exclusions: false, antinumerator: false, patient_id: record._id,
|
119
|
+
first: record.first, last: record.last, gender: record.gender,
|
120
|
+
birthdate: record.birthdate};
|
144
121
|
var patient = record._id;
|
145
122
|
if (population()) {
|
146
|
-
value.population
|
123
|
+
value.population = true;
|
147
124
|
if (denominator()) {
|
148
|
-
value.denominator
|
125
|
+
value.denominator = true;
|
149
126
|
if (numerator()) {
|
150
|
-
value.numerator
|
127
|
+
value.numerator = true;
|
151
128
|
} else if (exclusion()) {
|
152
|
-
value.exclusions
|
153
|
-
value.denominator
|
129
|
+
value.exclusions = true;
|
130
|
+
value.denominator = false;
|
154
131
|
} else {
|
155
|
-
value.antinumerator
|
132
|
+
value.antinumerator = true;
|
156
133
|
}
|
157
134
|
}
|
158
135
|
}
|
159
|
-
emit(
|
160
|
-
};
|
161
|
-
|
162
|
-
root.reduce = function (key, values) {
|
163
|
-
var total = {population: [], denominator: [], numerator: [], exclusions: [], antinumerator: []};
|
164
|
-
for (var i = 0; i < values.length; i++) {
|
165
|
-
total.population = total.population.concat(values[i].population);
|
166
|
-
total.denominator = total.denominator.concat(values[i].denominator);
|
167
|
-
total.numerator = total.numerator.concat(values[i].numerator);
|
168
|
-
total.exclusions = total.exclusions.concat(values[i].exclusions);
|
169
|
-
total.antinumerator = total.antinumerator.concat(values[i].antinumerator);
|
170
|
-
}
|
171
|
-
return total;
|
136
|
+
emit(ObjectId(), value);
|
172
137
|
};
|
173
138
|
|
174
|
-
}
|
139
|
+
}
|
@@ -1,3 +1,4 @@
|
|
1
|
+
function(){
|
1
2
|
// Underscore.js 1.1.2
|
2
3
|
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
|
3
4
|
// Underscore is freely distributable under the MIT license.
|
@@ -5,7 +6,7 @@
|
|
5
6
|
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
6
7
|
// For all details and documentation:
|
7
8
|
// http://documentcloud.github.com/underscore
|
8
|
-
|
9
|
+
var o=this,A=o._,r=typeof StopIteration!=="undefined"?StopIteration:"__break__",k=Array.prototype,m=Object.prototype,i=k.slice,B=k.unshift,C=m.toString,p=m.hasOwnProperty,s=k.forEach,t=k.map,u=k.reduce,v=k.reduceRight,w=k.filter,x=k.every,y=k.some,n=k.indexOf,z=k.lastIndexOf;m=Array.isArray;var D=Object.keys,c=function(a){return new l(a)};if(typeof exports!=="undefined")exports._=c;o._=c;c.VERSION="1.1.2";var j=c.each=c.forEach=function(a,b,d){try{if(s&&a.forEach===s)a.forEach(b,d);else if(c.isNumber(a.length))for(var e=
|
9
10
|
0,f=a.length;e<f;e++)b.call(d,a[e],e,a);else for(e in a)p.call(a,e)&&b.call(d,a[e],e,a)}catch(g){if(g!=r)throw g;}return a};c.map=function(a,b,d){if(t&&a.map===t)return a.map(b,d);var e=[];j(a,function(f,g,h){e[e.length]=b.call(d,f,g,h)});return e};c.reduce=c.foldl=c.inject=function(a,b,d,e){var f=d!==void 0;if(u&&a.reduce===u){if(e)b=c.bind(b,e);return f?a.reduce(b,d):a.reduce(b)}j(a,function(g,h,E){d=!f&&h===0?g:b.call(e,d,g,h,E)});return d};c.reduceRight=c.foldr=function(a,b,d,e){if(v&&a.reduceRight===
|
10
11
|
v){if(e)b=c.bind(b,e);return d!==void 0?a.reduceRight(b,d):a.reduceRight(b)}a=(c.isArray(a)?a.slice():c.toArray(a)).reverse();return c.reduce(a,b,d,e)};c.find=c.detect=function(a,b,d){var e;j(a,function(f,g,h){if(b.call(d,f,g,h)){e=f;c.breakLoop()}});return e};c.filter=c.select=function(a,b,d){if(w&&a.filter===w)return a.filter(b,d);var e=[];j(a,function(f,g,h){if(b.call(d,f,g,h))e[e.length]=f});return e};c.reject=function(a,b,d){var e=[];j(a,function(f,g,h){b.call(d,f,g,h)||(e[e.length]=f)});return e};
|
11
12
|
c.every=c.all=function(a,b,d){b=b||c.identity;if(x&&a.every===x)return a.every(b,d);var e=true;j(a,function(f,g,h){(e=e&&b.call(d,f,g,h))||c.breakLoop()});return e};c.some=c.any=function(a,b,d){b=b||c.identity;if(y&&a.some===y)return a.some(b,d);var e=false;j(a,function(f,g,h){if(e=b.call(d,f,g,h))c.breakLoop()});return e};c.include=c.contains=function(a,b){if(n&&a.indexOf===n)return a.indexOf(b)!=-1;var d=false;j(a,function(e){if(d=e===b)c.breakLoop()});return d};c.invoke=function(a,b){var d=i.call(arguments,
|
@@ -21,4 +22,4 @@ b.multiline;if(d!=="object")return false;if(a.length&&a.length!==b.length)return
|
|
21
22
|
a.callee)};c.isFunction=function(a){return!!(a&&a.constructor&&a.call&&a.apply)};c.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};c.isNumber=function(a){return a===+a||C.call(a)==="[object Number]"};c.isBoolean=function(a){return a===true||a===false};c.isDate=function(a){return!!(a&&a.getTimezoneOffset&&a.setUTCFullYear)};c.isRegExp=function(a){return!!(a&&a.test&&a.exec&&(a.ignoreCase||a.ignoreCase===false))};c.isNaN=function(a){return c.isNumber(a)&&isNaN(a)};c.isNull=function(a){return a===
|
22
23
|
null};c.isUndefined=function(a){return typeof a=="undefined"};c.noConflict=function(){o._=A;return this};c.identity=function(a){return a};c.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};c.breakLoop=function(){throw r;};c.mixin=function(a){j(c.functions(a),function(b){F(b,c[b]=a[b])})};var G=0;c.uniqueId=function(a){var b=G++;return a?a+b:b};c.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g};c.template=function(a,b){var d=c.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+
|
23
24
|
a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(e,f){return"',"+f.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||null,function(e,f){return"');"+f.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return b?d(b):d};var l=function(a){this._wrapped=a};c.prototype=l.prototype;var q=function(a,b){return b?c(a).chain():a},F=function(a,b){l.prototype[a]=function(){var d=
|
24
|
-
i.call(arguments);B.call(d,this._wrapped);return q(b.apply(c,d),this._chain)}};c.mixin(c);j(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=k[a];l.prototype[a]=function(){b.apply(this._wrapped,arguments);return q(this._wrapped,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];l.prototype[a]=function(){return q(b.apply(this._wrapped,arguments),this._chain)}});l.prototype.chain=function(){this._chain=true;return this};l.prototype.value=function(){return this._wrapped}}
|
25
|
+
i.call(arguments);B.call(d,this._wrapped);return q(b.apply(c,d),this._chain)}};c.mixin(c);j(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=k[a];l.prototype[a]=function(){b.apply(this._wrapped,arguments);return q(this._wrapped,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];l.prototype[a]=function(){return q(b.apply(this._wrapped,arguments),this._chain)}});l.prototype.chain=function(){this._chain=true;return this};l.prototype.value=function(){return this._wrapped}}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module QME
|
2
|
+
module DatabaseAccess
|
3
|
+
|
4
|
+
# Set up the information to connect to the database. Database host
|
5
|
+
# and port may be set using the environment variables TEST_DB_HOST
|
6
|
+
# and TEST_DB_PORT which default to localhost and 27017 respectively.
|
7
|
+
# @param [String] db_name the name of the database to use
|
8
|
+
def determine_connection_information(db_name = nil)
|
9
|
+
@db_name = ENV['DB_NAME'] || db_name || 'test'
|
10
|
+
@db_host = ENV['TEST_DB_HOST'] || 'localhost'
|
11
|
+
@db_port = ENV['TEST_DB_PORT'] ? ENV['TEST_DB_PORT'].to_i : 27017
|
12
|
+
end
|
13
|
+
|
14
|
+
# Directly inject an instance of a database
|
15
|
+
# @param [Mongo::Database] db the database that you would like the object to use
|
16
|
+
def inject_db(db)
|
17
|
+
@db = db
|
18
|
+
end
|
19
|
+
|
20
|
+
# Lazily creates a connection to the database and initializes the
|
21
|
+
# JavaScript environment
|
22
|
+
# @return [Mongo::Connection]
|
23
|
+
def get_db
|
24
|
+
if @db == nil
|
25
|
+
@db = Mongo::Connection.new(@db_host, @db_port).db(@db_name)
|
26
|
+
end
|
27
|
+
@db
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -7,11 +7,13 @@ module QME
|
|
7
7
|
'2.16.840.1.113883.6.1' => 'LOINC',
|
8
8
|
'2.16.840.1.113883.6.96' => 'SNOMED-CT',
|
9
9
|
'2.16.840.1.113883.6.12' => 'CPT',
|
10
|
+
#'2.16.840.1.113883.3.88.12.80.32' => 'CPT',
|
10
11
|
'2.16.840.1.113883.6.88' => 'RxNorm',
|
11
12
|
'2.16.840.1.113883.6.103' => 'ICD-9-CM',
|
12
13
|
'2.16.840.1.113883.6.104' => 'ICD-9-CM',
|
13
14
|
'2.16.840.1.113883.6.90' => 'ICD-10-CM',
|
14
|
-
'2.16.840.1.113883.6.14' => 'HCPCS'
|
15
|
+
'2.16.840.1.113883.6.14' => 'HCPCS',
|
16
|
+
'2.16.840.1.113883.6.59' => 'CVX'
|
15
17
|
}
|
16
18
|
|
17
19
|
# Returns the name of a code system given an oid
|
data/lib/qme/importer/entry.rb
CHANGED
@@ -2,15 +2,14 @@ module QME
|
|
2
2
|
module Importer
|
3
3
|
# Object that represents a CDA Entry (or act, observation, etc.)
|
4
4
|
class Entry
|
5
|
-
attr_accessor :start_time, :end_time, :time
|
6
|
-
attr_reader :
|
7
|
-
|
5
|
+
attr_accessor :start_time, :end_time, :time, :status
|
6
|
+
attr_reader :codes, :value
|
7
|
+
|
8
8
|
def initialize
|
9
9
|
@codes = {}
|
10
|
-
@status = {}
|
11
10
|
@value = {}
|
12
11
|
end
|
13
|
-
|
12
|
+
|
14
13
|
def Entry.from_event_hash(event)
|
15
14
|
entry = Entry.new
|
16
15
|
entry.add_code(event['code'], event['code_set'])
|
@@ -18,7 +17,7 @@ module QME
|
|
18
17
|
entry.set_value(event['value'], event['unit'])
|
19
18
|
entry
|
20
19
|
end
|
21
|
-
|
20
|
+
|
22
21
|
# Add a code into the Entry
|
23
22
|
# @param [String] code the code to add
|
24
23
|
# @param [String] code_system the code system that the code belongs to
|
@@ -26,15 +25,7 @@ module QME
|
|
26
25
|
@codes[code_system] ||= []
|
27
26
|
@codes[code_system] << code
|
28
27
|
end
|
29
|
-
|
30
|
-
# Set a status for the Entry
|
31
|
-
# @param [String] status_code the code to set
|
32
|
-
# @param [String] code_system the code system that the status_code belongs to
|
33
|
-
def set_status(status_code, code_system)
|
34
|
-
@status[:code] = status_code
|
35
|
-
@status[:code_system] = code_system
|
36
|
-
end
|
37
|
-
|
28
|
+
|
38
29
|
# Sets the value for the entry
|
39
30
|
# @param [String] scalar the value
|
40
31
|
# @param [String] units the units of the scalar value
|
@@ -42,24 +33,23 @@ module QME
|
|
42
33
|
@value[:scalar] = scalar
|
43
34
|
@value[:units] = units
|
44
35
|
end
|
45
|
-
|
36
|
+
|
46
37
|
# Checks if a code is in the list of possible codes
|
47
38
|
# @param [Array] code_set an Array of Hashes that describe the values for code sets
|
48
39
|
# @return [true, false] whether the code is in the list of desired codes
|
49
40
|
def is_in_code_set?(code_set)
|
50
41
|
@codes.keys.each do |code_system|
|
51
|
-
|
52
|
-
|
42
|
+
all_codes_in_system = code_set.find_all {|set| set['set'] == code_system}
|
43
|
+
all_codes_in_system.each do |codes_in_system|
|
53
44
|
matching_codes = codes_in_system['values'] & @codes[code_system]
|
54
45
|
if matching_codes.length > 0
|
55
46
|
return true
|
56
47
|
end
|
57
48
|
end
|
58
49
|
end
|
59
|
-
|
60
50
|
false
|
61
51
|
end
|
62
|
-
|
52
|
+
|
63
53
|
# Tries to find a single point in time for this entry. Will first return time if it is present,
|
64
54
|
# then fall back to start_time and finally end_time
|
65
55
|
def as_point_in_time
|
@@ -71,13 +61,13 @@ module QME
|
|
71
61
|
@end_time
|
72
62
|
end
|
73
63
|
end
|
74
|
-
|
64
|
+
|
75
65
|
# Checks to see if this Entry can be used as a date range
|
76
66
|
# @return [true, false] If the Entry has a start and end time returns true, false otherwise.
|
77
67
|
def is_date_range?
|
78
68
|
(! @start_time.nil?) && (! @end_time.nil?)
|
79
69
|
end
|
80
|
-
|
70
|
+
|
81
71
|
# Checks to see if this Entry is usable for measure calculation. This means that it contains
|
82
72
|
# at least one code and has one of its time properties set (start, end or time)
|
83
73
|
# @return [true, false]
|