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.
Files changed (52) hide show
  1. data/Gemfile +9 -9
  2. data/README.md +39 -2
  3. data/Rakefile +25 -44
  4. data/js/map-reduce-utils.js +174 -0
  5. data/js/underscore-min.js +24 -0
  6. data/lib/qme/importer/code_system_helper.rb +26 -0
  7. data/lib/qme/importer/entry.rb +89 -0
  8. data/lib/qme/importer/generic_importer.rb +71 -0
  9. data/lib/qme/importer/hl7_helper.rb +27 -0
  10. data/lib/qme/importer/patient_importer.rb +150 -0
  11. data/lib/qme/importer/property_matcher.rb +103 -0
  12. data/lib/qme/importer/section_importer.rb +82 -0
  13. data/lib/qme/map/map_reduce_builder.rb +77 -147
  14. data/lib/qme/map/map_reduce_executor.rb +97 -13
  15. data/lib/qme/measure/database_loader.rb +90 -0
  16. data/lib/qme/measure/measure_loader.rb +141 -0
  17. data/lib/qme/mongo_helpers.rb +15 -0
  18. data/lib/qme/randomizer/patient_randomizer.rb +95 -0
  19. data/lib/qme_test.rb +13 -0
  20. data/lib/quality-measure-engine.rb +20 -4
  21. data/lib/tasks/measure.rake +76 -0
  22. data/lib/tasks/mongo.rake +74 -0
  23. data/lib/tasks/patient_random.rake +46 -0
  24. metadata +110 -156
  25. data/.gitignore +0 -6
  26. data/Gemfile.lock +0 -44
  27. data/fixtures/complex_measure.json +0 -36
  28. data/fixtures/result_example.json +0 -6
  29. data/lib/patches/v8.rb +0 -20
  30. data/lib/qme/query/json_document_builder.rb +0 -130
  31. data/lib/qme/query/json_query_executor.rb +0 -44
  32. data/measures/0032/0032_NQF_Cervical_Cancer_Screening.json +0 -171
  33. data/measures/0032/patients/denominator1.json +0 -10
  34. data/measures/0032/patients/denominator2.json +0 -10
  35. data/measures/0032/patients/numerator1.json +0 -11
  36. data/measures/0032/patients/population1.json +0 -9
  37. data/measures/0032/patients/population2.json +0 -11
  38. data/measures/0032/result/result.json +0 -6
  39. data/measures/0043/0043_NQF_PneumoniaVaccinationStatusForOlderAdults.json +0 -71
  40. data/measures/0043/patients/denominator.json +0 -11
  41. data/measures/0043/patients/numerator.json +0 -11
  42. data/measures/0043/patients/population.json +0 -10
  43. data/measures/0043/result/result.json +0 -6
  44. data/quality-measure-engine.gemspec +0 -97
  45. data/schema/result.json +0 -28
  46. data/schema/schema.json +0 -143
  47. data/spec/qme/map/map_reduce_builder_spec.rb +0 -64
  48. data/spec/qme/measures_spec.rb +0 -50
  49. data/spec/qme/query/json_document_builder_spec.rb +0 -56
  50. data/spec/schema_spec.rb +0 -21
  51. data/spec/spec_helper.rb +0 -7
  52. data/spec/validate_measures_spec.rb +0 -21
data/Gemfile CHANGED
@@ -1,17 +1,17 @@
1
- source :gemcutter
1
+ source "http://rubygems.org"
2
2
 
3
- gem 'mongo'
4
- gem 'bson_ext'
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 'rspec'
10
- gem 'jsonschema'
11
- gem 'awesome_print', :require => 'ap'
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 'jeweler'
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 necessay gems:
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 'jeweler'
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
- Jeweler::Tasks.new do |gem|
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
- desc 'Load all the measures and sample patient files into the database'
27
- task :loaddb do
28
- require './spec/spec_helper'
29
- db_host = nil
30
- if ENV['TEST_DB_HOST']
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
- db.drop_collection('measures')
38
- db.drop_collection('records')
25
+ end
39
26
 
40
- measures = Dir.glob('measures/*')
41
- measures.each do |dir|
42
- # load db with measure and sample patient records
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