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.
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