quality-measure-engine 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -9
- data/README.md +39 -2
- data/Rakefile +25 -44
- data/js/map-reduce-utils.js +174 -0
- data/js/underscore-min.js +24 -0
- data/lib/qme/importer/code_system_helper.rb +26 -0
- data/lib/qme/importer/entry.rb +89 -0
- data/lib/qme/importer/generic_importer.rb +71 -0
- data/lib/qme/importer/hl7_helper.rb +27 -0
- data/lib/qme/importer/patient_importer.rb +150 -0
- data/lib/qme/importer/property_matcher.rb +103 -0
- data/lib/qme/importer/section_importer.rb +82 -0
- data/lib/qme/map/map_reduce_builder.rb +77 -147
- data/lib/qme/map/map_reduce_executor.rb +97 -13
- data/lib/qme/measure/database_loader.rb +90 -0
- data/lib/qme/measure/measure_loader.rb +141 -0
- data/lib/qme/mongo_helpers.rb +15 -0
- data/lib/qme/randomizer/patient_randomizer.rb +95 -0
- data/lib/qme_test.rb +13 -0
- data/lib/quality-measure-engine.rb +20 -4
- data/lib/tasks/measure.rake +76 -0
- data/lib/tasks/mongo.rake +74 -0
- data/lib/tasks/patient_random.rake +46 -0
- metadata +110 -156
- data/.gitignore +0 -6
- data/Gemfile.lock +0 -44
- data/fixtures/complex_measure.json +0 -36
- data/fixtures/result_example.json +0 -6
- data/lib/patches/v8.rb +0 -20
- data/lib/qme/query/json_document_builder.rb +0 -130
- data/lib/qme/query/json_query_executor.rb +0 -44
- data/measures/0032/0032_NQF_Cervical_Cancer_Screening.json +0 -171
- data/measures/0032/patients/denominator1.json +0 -10
- data/measures/0032/patients/denominator2.json +0 -10
- data/measures/0032/patients/numerator1.json +0 -11
- data/measures/0032/patients/population1.json +0 -9
- data/measures/0032/patients/population2.json +0 -11
- data/measures/0032/result/result.json +0 -6
- data/measures/0043/0043_NQF_PneumoniaVaccinationStatusForOlderAdults.json +0 -71
- data/measures/0043/patients/denominator.json +0 -11
- data/measures/0043/patients/numerator.json +0 -11
- data/measures/0043/patients/population.json +0 -10
- data/measures/0043/result/result.json +0 -6
- data/quality-measure-engine.gemspec +0 -97
- data/schema/result.json +0 -28
- data/schema/schema.json +0 -143
- data/spec/qme/map/map_reduce_builder_spec.rb +0 -64
- data/spec/qme/measures_spec.rb +0 -50
- data/spec/qme/query/json_document_builder_spec.rb +0 -56
- data/spec/schema_spec.rb +0 -21
- data/spec/spec_helper.rb +0 -7
- data/spec/validate_measures_spec.rb +0 -21
data/Gemfile
CHANGED
@@ -1,17 +1,17 @@
|
|
1
|
-
source
|
1
|
+
source "http://rubygems.org"
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
gemspec :development_group => :test
|
4
|
+
|
5
|
+
gem 'bson_ext', :platforms => :mri
|
5
6
|
gem 'rake'
|
6
|
-
gem 'therubyracer', :require => 'v8'
|
7
7
|
|
8
8
|
group :test do
|
9
|
-
gem '
|
10
|
-
gem '
|
11
|
-
gem '
|
9
|
+
gem 'cover_me', '>= 1.0.0.rc5', :platforms => :ruby_19
|
10
|
+
gem 'metric_fu'
|
11
|
+
gem 'sinatra'
|
12
12
|
end
|
13
13
|
|
14
14
|
group :build do
|
15
|
-
gem '
|
15
|
+
gem 'yard'
|
16
|
+
gem 'kramdown' # needed by yard
|
16
17
|
end
|
17
|
-
|
data/README.md
CHANGED
@@ -7,16 +7,53 @@ This project currently uses Ruby 1.9.2 and is built using [Bundler](http://gembu
|
|
7
7
|
|
8
8
|
gem install bundler
|
9
9
|
|
10
|
-
Then run bundler to grab all of the
|
10
|
+
Then run bundler to grab all of the necessary gems:
|
11
11
|
|
12
12
|
bundle install
|
13
13
|
|
14
|
+
The Quality Measure engine relies on a MongoDB [MongoDB](http://www.mongodb.org/) running a minimum of version 1.6.* or higher. To get and install Mongo refer to :
|
15
|
+
|
16
|
+
http://www.mongodb.org/display/DOCS/Quickstart
|
17
|
+
|
14
18
|
Testing
|
15
19
|
-------
|
16
20
|
|
17
21
|
This project uses [RSpec](http://github.com/rspec/rspec-core) for testing. To run the suite, just enter the following:
|
18
22
|
|
19
|
-
rake spec
|
23
|
+
bundle exec rake spec
|
24
|
+
|
25
|
+
The coverage of the test suite is monitored with [cover_me](https://github.com/markbates/cover_me) and can be run with:
|
26
|
+
|
27
|
+
bundle exec rake coverage
|
28
|
+
|
29
|
+
Map Reduce Testing
|
30
|
+
------------------
|
31
|
+
|
32
|
+
This project used the [MapReduce](http://www.mongodb.org/display/DOCS/MapReduce) functionality of MongoDB pretty heavily.
|
33
|
+
Debugging JavaScript that is to be run inside of MongoDB can be a bit of a chore, so there is a testing tool that can be run
|
34
|
+
in your browser to aid in troubleshooting.
|
35
|
+
|
36
|
+
The tool is a very small web application based on the [Sinatra](http://www.sinatrarb.com/) framework. It can be run
|
37
|
+
by executing the following command:
|
38
|
+
|
39
|
+
bundle exec ruby map_test/map_test.rb
|
40
|
+
|
41
|
+
After running this command, you can open your browser to [http://localhost:4567](http://localhost:4567). This will show you a page
|
42
|
+
of measures to choose from. Once you have selected a measure, it will take you to a page where you can choose the map function you
|
43
|
+
want to test and the effective date you want to run the function with. Once that is selected, you will arrive at the map test page.
|
44
|
+
The map test page provides the ability to load test JSON records from within the project. Once they are loaded, they can be edited
|
45
|
+
in the textarea on the page. Finally, if you click the "run" button, it will execute the map function on the record in the text area
|
46
|
+
and output the results. Since this is executing in a web browser, you can use the JavaScript debugging utilities provided to set
|
47
|
+
breakpoints and inspect variables.
|
48
|
+
|
49
|
+
Source Code Analysis
|
50
|
+
--------------------
|
51
|
+
|
52
|
+
This project uses [metric_fu](http://metric-fu.rubyforge.org/) for source code analysis. Reports can be run with:
|
53
|
+
|
54
|
+
bundle exec rake metrics:all
|
55
|
+
|
56
|
+
The project is currently configured to run Flay, Flog, Churn, Reek and Roodi
|
20
57
|
|
21
58
|
Project Practices
|
22
59
|
------------------
|
data/Rakefile
CHANGED
@@ -1,56 +1,37 @@
|
|
1
1
|
require 'rspec/core/rake_task'
|
2
|
-
require '
|
2
|
+
require 'yard'
|
3
|
+
require 'metric_fu'
|
4
|
+
|
5
|
+
ENV['MEASURE_DIR'] = ENV['MEASURE_DIR'] || File.join('fixtures', 'measure_defs')
|
6
|
+
|
7
|
+
Dir['lib/tasks/*.rake'].sort.each do |ext|
|
8
|
+
load ext
|
9
|
+
end
|
3
10
|
|
4
11
|
RSpec::Core::RakeTask.new do |t|
|
5
12
|
t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
|
6
13
|
t.pattern = 'spec/**/*_spec.rb'
|
7
14
|
end
|
8
15
|
|
9
|
-
|
10
|
-
gem.name = "quality-measure-engine"
|
11
|
-
gem.summary = "A library for extracting quality measure information from HITSP C32's and ASTM CCR's"
|
12
|
-
gem.description = "A library for extracting quality measure information from HITSP C32's and ASTM CCR's"
|
13
|
-
gem.email = "talk@projectpophealth.org"
|
14
|
-
gem.homepage = "http://github.com/pophealth/quality-measure-engine"
|
15
|
-
gem.authors = ["Marc Hadley", "Andy Gregorowicz"]
|
16
|
-
|
17
|
-
gem.add_dependency 'mongo', '~> 1.1'
|
18
|
-
gem.add_dependency 'therubyracer', '~> 0.7.5'
|
19
|
-
|
20
|
-
gem.add_development_dependency "jsonschema", "~> 2.0.0"
|
21
|
-
gem.add_development_dependency "rspec", "~> 2.0.0"
|
22
|
-
gem.add_development_dependency "awesome_print", "~> 0.2.1"
|
23
|
-
gem.add_development_dependency "jeweler", "~> 1.4.0"
|
24
|
-
end
|
16
|
+
YARD::Rake::YardocTask.new
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
db_host = ENV['TEST_DB_HOST']
|
32
|
-
else
|
33
|
-
db_host = 'localhost'
|
18
|
+
namespace :cover_me do
|
19
|
+
|
20
|
+
task :report do
|
21
|
+
require 'cover_me'
|
22
|
+
CoverMe.complete!
|
34
23
|
end
|
35
|
-
db = Mongo::Connection.new(db_host, 27017).db('test')
|
36
24
|
|
37
|
-
|
38
|
-
db.drop_collection('records')
|
25
|
+
end
|
39
26
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
files = Dir.glob(File.join(dir,'*.json'))
|
44
|
-
measure_file = files[0]
|
45
|
-
patient_files = Dir.glob(File.join(dir, 'patients', '*.json'))
|
46
|
-
measure = JSON.parse(File.read(measure_file))
|
47
|
-
measure_id = measure['id']
|
48
|
-
measure_collection = db.create_collection('measures')
|
49
|
-
record_collection = db.create_collection('records')
|
50
|
-
measure_collection.save(measure)
|
51
|
-
patient_files.each do |patient_file|
|
52
|
-
patient = JSON.parse(File.read(patient_file))
|
53
|
-
record_collection.save(patient)
|
54
|
-
end
|
55
|
-
end
|
27
|
+
task :coverage do
|
28
|
+
Rake::Task['spec'].invoke
|
29
|
+
Rake::Task['cover_me:report'].invoke
|
56
30
|
end
|
31
|
+
|
32
|
+
MetricFu::Configuration.run do |config|
|
33
|
+
#define which metrics you want to use
|
34
|
+
config.metrics = [:roodi, :reek, :churn, :flog, :flay]
|
35
|
+
config.graphs = [:flog, :flay]
|
36
|
+
config.flay ={:dirs_to_flay => []} #Flay doesn't seem to be handling CLI arguments well... so this config squashes them
|
37
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
// Adds common utility functions to the root JS object. These are then
|
2
|
+
// available for use by the map-reduce functions for each measure.
|
3
|
+
// lib/qme/mongo_helpers.rb executes this function on a database
|
4
|
+
// connection.
|
5
|
+
(function() {
|
6
|
+
|
7
|
+
var root = this;
|
8
|
+
|
9
|
+
// returns the number of values which fall between the supplied limits
|
10
|
+
// value may be a number or an array of numbers
|
11
|
+
root.inRange = function(value, min, max) {
|
12
|
+
if (!_.isArray(value))
|
13
|
+
value = [value];
|
14
|
+
var count = 0;
|
15
|
+
for (i=0;i<value.length;i++) {
|
16
|
+
if ((value[i]>=min) && (value[i]<=max))
|
17
|
+
count++;
|
18
|
+
}
|
19
|
+
return count;
|
20
|
+
};
|
21
|
+
|
22
|
+
// returns the largest member of value that is within the supplied range
|
23
|
+
root.maxInRange = function(value, min, max) {
|
24
|
+
if (value==null)
|
25
|
+
return null;
|
26
|
+
var allInRange = _.select(value, function(v) {return v>=min && v<=max;});
|
27
|
+
return _.max(allInRange);
|
28
|
+
}
|
29
|
+
|
30
|
+
// returns the number of values which are less than the supplied limit
|
31
|
+
// value may be a number or an array of numbers
|
32
|
+
root.lessThan = function(value, max) {
|
33
|
+
if (!_.isArray(value))
|
34
|
+
value = [value];
|
35
|
+
var count = 0;
|
36
|
+
for (i=0;i<value.length;i++) {
|
37
|
+
if (value[i]<=max)
|
38
|
+
count++;
|
39
|
+
}
|
40
|
+
return count;
|
41
|
+
};
|
42
|
+
|
43
|
+
// Returns the a boolean true when any entry within conditions[i].end is
|
44
|
+
// ever less than the endDate. If no conditions meet this criteria, this
|
45
|
+
// function always returns false
|
46
|
+
root.conditionResolved = function(conditions, startDate, endDate) {
|
47
|
+
if (conditions) {
|
48
|
+
return _.any(conditions, function(condition) {
|
49
|
+
return inRange(condition.end, startDate, endDate) > 0;
|
50
|
+
});
|
51
|
+
} else {
|
52
|
+
return false
|
53
|
+
};
|
54
|
+
};
|
55
|
+
|
56
|
+
// Returns the minimum of readings[i].value where readings[i].date is in
|
57
|
+
// the supplied startDate and endDate. If no reading meet this criteria,
|
58
|
+
// returns defaultValue.
|
59
|
+
root.minValueInDateRange = function(readings, startDate, endDate, defaultValue) {
|
60
|
+
var readingInDateRange = function(reading) {
|
61
|
+
var result = inRange(reading.date, startDate, endDate);
|
62
|
+
return result;
|
63
|
+
};
|
64
|
+
|
65
|
+
if (!readings || readings.length<1)
|
66
|
+
return defaultValue;
|
67
|
+
|
68
|
+
var allInDateRange = _.select(readings, readingInDateRange);
|
69
|
+
var min = _.min(allInDateRange, function(reading) {return reading.value;});
|
70
|
+
if (min)
|
71
|
+
return min.value;
|
72
|
+
else
|
73
|
+
return defaultValue;
|
74
|
+
};
|
75
|
+
|
76
|
+
// Returns the most recent readings[i].value where readings[i].date is in
|
77
|
+
// the supplied startDate and endDate. If no reading meet this criteria,
|
78
|
+
// returns defaultValue.
|
79
|
+
root.latestValueInDateRange = function(readings, startDate, endDate, defaultValue) {
|
80
|
+
var readingInDateRange = function(reading) {
|
81
|
+
var result = inRange(reading.date, startDate, endDate);
|
82
|
+
return result;
|
83
|
+
};
|
84
|
+
|
85
|
+
if (!readings || readings.length<1)
|
86
|
+
return defaultValue;
|
87
|
+
|
88
|
+
var allInDateRange = _.select(readings, readingInDateRange);
|
89
|
+
var latest = _.max(allInDateRange, function(reading) {return reading.date;});
|
90
|
+
if (latest)
|
91
|
+
return latest.value;
|
92
|
+
else
|
93
|
+
return defaultValue;
|
94
|
+
};
|
95
|
+
|
96
|
+
// Returns the number of actions that occur within the specified time period of
|
97
|
+
// something. The first two arguments are arrays or single-valued time stamps in
|
98
|
+
// seconds-since-the-epoch, timePeriod is in seconds.
|
99
|
+
root.actionFollowingSomething = function(something, action, timePeriod) {
|
100
|
+
if (!_.isArray(something))
|
101
|
+
something = [something];
|
102
|
+
if (!_.isArray(action))
|
103
|
+
action = [action];
|
104
|
+
|
105
|
+
var result = 0;
|
106
|
+
for (i=0; i<something.length; i++) {
|
107
|
+
var timeStamp = something[i];
|
108
|
+
for (j=0; j<action.length;j++) {
|
109
|
+
if (action[j]>=timeStamp && (action[j] <= (timeStamp+timePeriod)))
|
110
|
+
result++;
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
114
|
+
return result;
|
115
|
+
}
|
116
|
+
|
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
|
+
// Returns all members of the values array that fall between min and max inclusive
|
138
|
+
root.selectWithinRange = function(values, min, max) {
|
139
|
+
return _.select(values, function(value) { return value<=max && value>=min; });
|
140
|
+
}
|
141
|
+
|
142
|
+
root.map = function(record, population, denominator, numerator, exclusion) {
|
143
|
+
var value = {population: [], denominator: [], numerator: [], exclusions: [], antinumerator: []};
|
144
|
+
var patient = record._id;
|
145
|
+
if (population()) {
|
146
|
+
value.population.push(patient);
|
147
|
+
if (denominator()) {
|
148
|
+
value.denominator.push(patient);
|
149
|
+
if (numerator()) {
|
150
|
+
value.numerator.push(patient);
|
151
|
+
} else if (exclusion()) {
|
152
|
+
value.exclusions.push(patient);
|
153
|
+
value.denominator.pop();
|
154
|
+
} else {
|
155
|
+
value.antinumerator.push(patient);
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
emit(null, value);
|
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;
|
172
|
+
};
|
173
|
+
|
174
|
+
})();
|
@@ -0,0 +1,24 @@
|
|
1
|
+
// Underscore.js 1.1.2
|
2
|
+
// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
|
3
|
+
// Underscore is freely distributable under the MIT license.
|
4
|
+
// Portions of Underscore are inspired or borrowed from Prototype,
|
5
|
+
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
6
|
+
// For all details and documentation:
|
7
|
+
// http://documentcloud.github.com/underscore
|
8
|
+
(function(){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
|
+
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
|
+
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
|
+
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,
|
12
|
+
2);return c.map(a,function(e){return(b?e[b]:e).apply(e,d)})};c.pluck=function(a,b){return c.map(a,function(d){return d[b]})};c.max=function(a,b,d){if(!b&&c.isArray(a))return Math.max.apply(Math,a);var e={computed:-Infinity};j(a,function(f,g,h){g=b?b.call(d,f,g,h):f;g>=e.computed&&(e={value:f,computed:g})});return e.value};c.min=function(a,b,d){if(!b&&c.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};j(a,function(f,g,h){g=b?b.call(d,f,g,h):f;g<e.computed&&(e={value:f,computed:g})});
|
13
|
+
return e.value};c.sortBy=function(a,b,d){return c.pluck(c.map(a,function(e,f,g){return{value:e,criteria:b.call(d,e,f,g)}}).sort(function(e,f){var g=e.criteria,h=f.criteria;return g<h?-1:g>h?1:0}),"value")};c.sortedIndex=function(a,b,d){d=d||c.identity;for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(b)?e=g+1:f=g}return e};c.toArray=function(a){if(!a)return[];if(a.toArray)return a.toArray();if(c.isArray(a))return a;if(c.isArguments(a))return i.call(a);return c.values(a)};c.size=function(a){return c.toArray(a).length};
|
14
|
+
c.first=c.head=function(a,b,d){return b&&!d?i.call(a,0,b):a[0]};c.rest=c.tail=function(a,b,d){return i.call(a,c.isUndefined(b)||d?1:b)};c.last=function(a){return a[a.length-1]};c.compact=function(a){return c.filter(a,function(b){return!!b})};c.flatten=function(a){return c.reduce(a,function(b,d){if(c.isArray(d))return b.concat(c.flatten(d));b[b.length]=d;return b},[])};c.without=function(a){var b=i.call(arguments,1);return c.filter(a,function(d){return!c.include(b,d)})};c.uniq=c.unique=function(a,
|
15
|
+
b){return c.reduce(a,function(d,e,f){if(0==f||(b===true?c.last(d)!=e:!c.include(d,e)))d[d.length]=e;return d},[])};c.intersect=function(a){var b=i.call(arguments,1);return c.filter(c.uniq(a),function(d){return c.every(b,function(e){return c.indexOf(e,d)>=0})})};c.zip=function(){for(var a=i.call(arguments),b=c.max(c.pluck(a,"length")),d=Array(b),e=0;e<b;e++)d[e]=c.pluck(a,""+e);return d};c.indexOf=function(a,b){if(n&&a.indexOf===n)return a.indexOf(b);for(var d=0,e=a.length;d<e;d++)if(a[d]===b)return d;
|
16
|
+
return-1};c.lastIndexOf=function(a,b){if(z&&a.lastIndexOf===z)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};c.range=function(a,b,d){var e=i.call(arguments),f=e.length<=1;a=f?0:e[0];b=f?e[0]:e[1];d=e[2]||1;e=Math.max(Math.ceil((b-a)/d),0);f=0;for(var g=Array(e);f<e;){g[f++]=a;a+=d}return g};c.bind=function(a,b){var d=i.call(arguments,2);return function(){return a.apply(b||{},d.concat(i.call(arguments)))}};c.bindAll=function(a){var b=i.call(arguments,1);if(b.length==
|
17
|
+
0)b=c.functions(a);j(b,function(d){a[d]=c.bind(a[d],a)});return a};c.memoize=function(a,b){var d={};b=b||c.identity;return function(){var e=b.apply(this,arguments);return e in d?d[e]:d[e]=a.apply(this,arguments)}};c.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};c.defer=function(a){return c.delay.apply(c,[a,1].concat(i.call(arguments,1)))};c.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments));return b.apply(b,d)}};c.compose=
|
18
|
+
function(){var a=i.call(arguments);return function(){for(var b=i.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};c.keys=D||function(a){if(c.isArray(a))return c.range(0,a.length);var b=[],d;for(d in a)if(p.call(a,d))b[b.length]=d;return b};c.values=function(a){return c.map(a,c.identity)};c.functions=c.methods=function(a){return c.filter(c.keys(a),function(b){return c.isFunction(a[b])}).sort()};c.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});
|
19
|
+
return a};c.clone=function(a){return c.isArray(a)?a.slice():c.extend({},a)};c.tap=function(a,b){b(a);return a};c.isEqual=function(a,b){if(a===b)return true;var d=typeof a;if(d!=typeof b)return false;if(a==b)return true;if(!a&&b||a&&!b)return false;if(a.isEqual)return a.isEqual(b);if(c.isDate(a)&&c.isDate(b))return a.getTime()===b.getTime();if(c.isNaN(a)&&c.isNaN(b))return false;if(c.isRegExp(a)&&c.isRegExp(b))return a.source===b.source&&a.global===b.global&&a.ignoreCase===b.ignoreCase&&a.multiline===
|
20
|
+
b.multiline;if(d!=="object")return false;if(a.length&&a.length!==b.length)return false;d=c.keys(a);var e=c.keys(b);if(d.length!=e.length)return false;for(var f in a)if(!(f in b)||!c.isEqual(a[f],b[f]))return false;return true};c.isEmpty=function(a){if(c.isArray(a)||c.isString(a))return a.length===0;for(var b in a)if(p.call(a,b))return false;return true};c.isElement=function(a){return!!(a&&a.nodeType==1)};c.isArray=m||function(a){return!!(a&&a.concat&&a.unshift&&!a.callee)};c.isArguments=function(a){return!!(a&&
|
21
|
+
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
|
+
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
|
+
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}})();
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
|
4
|
+
# General helpers for working with codes and code systems
|
5
|
+
class CodeSystemHelper
|
6
|
+
CODE_SYSTEMS = {
|
7
|
+
'2.16.840.1.113883.6.1' => 'LOINC',
|
8
|
+
'2.16.840.1.113883.6.96' => 'SNOMED-CT',
|
9
|
+
'2.16.840.1.113883.6.12' => 'CPT',
|
10
|
+
'2.16.840.1.113883.6.88' => 'RxNorm',
|
11
|
+
'2.16.840.1.113883.6.103' => 'ICD-9-CM',
|
12
|
+
'2.16.840.1.113883.6.104' => 'ICD-9-CM',
|
13
|
+
'2.16.840.1.113883.6.90' => 'ICD-10-CM',
|
14
|
+
'2.16.840.1.113883.6.14' => 'HCPCS'
|
15
|
+
}
|
16
|
+
|
17
|
+
# Returns the name of a code system given an oid
|
18
|
+
# @param [String] oid of a code system
|
19
|
+
# @return [String] the name of the code system as described in the measure definition JSON
|
20
|
+
def self.code_system_for(oid)
|
21
|
+
CODE_SYSTEMS[oid]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
# Object that represents a CDA Entry (or act, observation, etc.)
|
4
|
+
class Entry
|
5
|
+
attr_accessor :start_time, :end_time, :time
|
6
|
+
attr_reader :status, :codes, :value
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@codes = {}
|
10
|
+
@status = {}
|
11
|
+
@value = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def Entry.from_event_hash(event)
|
15
|
+
entry = Entry.new
|
16
|
+
entry.add_code(event['code'], event['code_set'])
|
17
|
+
entry.time = event['time']
|
18
|
+
entry.set_value(event['value'], event['unit'])
|
19
|
+
entry
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add a code into the Entry
|
23
|
+
# @param [String] code the code to add
|
24
|
+
# @param [String] code_system the code system that the code belongs to
|
25
|
+
def add_code(code, code_system)
|
26
|
+
@codes[code_system] ||= []
|
27
|
+
@codes[code_system] << code
|
28
|
+
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
|
+
|
38
|
+
# Sets the value for the entry
|
39
|
+
# @param [String] scalar the value
|
40
|
+
# @param [String] units the units of the scalar value
|
41
|
+
def set_value(scalar, units=nil)
|
42
|
+
@value[:scalar] = scalar
|
43
|
+
@value[:units] = units
|
44
|
+
end
|
45
|
+
|
46
|
+
# Checks if a code is in the list of possible codes
|
47
|
+
# @param [Array] code_set an Array of Hashes that describe the values for code sets
|
48
|
+
# @return [true, false] whether the code is in the list of desired codes
|
49
|
+
def is_in_code_set?(code_set)
|
50
|
+
@codes.keys.each do |code_system|
|
51
|
+
codes_in_system = code_set.find {|set| set['set'] == code_system}
|
52
|
+
if codes_in_system
|
53
|
+
matching_codes = codes_in_system['values'] & @codes[code_system]
|
54
|
+
if matching_codes.length > 0
|
55
|
+
return true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
# Tries to find a single point in time for this entry. Will first return time if it is present,
|
64
|
+
# then fall back to start_time and finally end_time
|
65
|
+
def as_point_in_time
|
66
|
+
if @time
|
67
|
+
@time
|
68
|
+
elsif @start_time
|
69
|
+
@start_time
|
70
|
+
else
|
71
|
+
@end_time
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Checks to see if this Entry can be used as a date range
|
76
|
+
# @return [true, false] If the Entry has a start and end time returns true, false otherwise.
|
77
|
+
def is_date_range?
|
78
|
+
(! @start_time.nil?) && (! @end_time.nil?)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Checks to see if this Entry is usable for measure calculation. This means that it contains
|
82
|
+
# at least one code and has one of its time properties set (start, end or time)
|
83
|
+
# @return [true, false]
|
84
|
+
def usable?
|
85
|
+
(! @codes.empty?) && ((! @start_time.nil?) || (! @end_time.nil?) || (! @time.nil?))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|