quality-measure-engine 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|