bonnie_bundler 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +12 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +267 -0
- data/README.md +4 -0
- data/Rakefile +29 -0
- data/bonnie-bundler.gemspec +29 -0
- data/config/initializers/mongo.rb +1 -0
- data/config/measures/measures_2_4_0.yml +719 -0
- data/config/mongoid.yml +6 -0
- data/lib/bonnie_bundler.rb +39 -0
- data/lib/ext/hash.rb +28 -0
- data/lib/ext/railtie.rb +11 -0
- data/lib/ext/valueset.rb +11 -0
- data/lib/measures/cql_to_elm_helper.rb +90 -0
- data/lib/measures/elm_parser.rb +74 -0
- data/lib/measures/loading/base_loader_definition.rb +61 -0
- data/lib/measures/loading/cql_loader.rb +420 -0
- data/lib/measures/loading/exceptions.rb +10 -0
- data/lib/measures/loading/loader.rb +178 -0
- data/lib/measures/loading/value_set_loader.rb +137 -0
- data/lib/measures/logic_extractor.rb +552 -0
- data/lib/measures/mongo_hash_key_wrapper.rb +44 -0
- data/lib/models/cql_measure.rb +160 -0
- data/lib/models/measure.rb +330 -0
- data/test/fixtures/BCS_v5_0_Artifacts.zip +0 -0
- data/test/fixtures/CMS158_v5_4_Artifacts.zip +0 -0
- data/test/fixtures/CMS158_v5_4_Artifacts_Update.zip +0 -0
- data/test/fixtures/DRAFT_CMS2_CQL.zip +0 -0
- data/test/fixtures/bonnienesting01_fixed.zip +0 -0
- data/test/fixtures/vcr_cassettes/mat_5-4_cql_export_vsac_response.yml +4723 -0
- data/test/fixtures/vcr_cassettes/multi_library_webcalls.yml +1892 -0
- data/test/fixtures/vcr_cassettes/valid_translation_response.yml +1120 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response.yml +1678 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_158.yml +1670 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_158_update.yml +1670 -0
- data/test/fixtures/vcr_cassettes/valid_vsac_response_includes_draft.yml +3480 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_no_profile_version.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_profile.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_draft_verion.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_no_profile_version.yml +1198 -0
- data/test/fixtures/vcr_cassettes/vs_loading_profile.yml +1196 -0
- data/test/fixtures/vcr_cassettes/vs_loading_version.yml +20331 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts.zip +0 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_Version.zip +0 -0
- data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_With_Profiles.zip +0 -0
- data/test/simplecov_init.rb +18 -0
- data/test/test_helper.rb +44 -0
- data/test/unit/load_mat_export_test.rb +181 -0
- data/test/unit/measure_complexity_test.rb +32 -0
- data/test/unit/measure_diff_test.rb +68 -0
- data/test/unit/mongo_hash_key_wrapper_test.rb +247 -0
- data/test/unit/storing_mat_export_package_test.rb +45 -0
- data/test/unit/value_set_loading_test.rb +109 -0
- data/test/vcr_setup.rb +20 -0
- metadata +258 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module Measures
|
2
|
+
# keys can't contain periods https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names
|
3
|
+
# technically they can't start with dollar sign either, but that's prohibited for CQL naming
|
4
|
+
# so:
|
5
|
+
# periods are converted to '^p'
|
6
|
+
# and carets are converted to '^c' therefore we aren't invalidating the use of any characters (e.g. caret)
|
7
|
+
class MongoHashKeyWrapper
|
8
|
+
|
9
|
+
def self.wrapKeys(theHash)
|
10
|
+
newKeys = Hash.new
|
11
|
+
theHash.keys.each do |key|
|
12
|
+
if (key.include? '.') || (key.include? '^')
|
13
|
+
newKeys[key] = key.gsub(/[\^\.]/, '^' => '^c', '.' => '^p')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
newKeys.each { |old, new| theHash[new] = theHash.delete old}
|
17
|
+
# now recurse on any contained hashes
|
18
|
+
theHash.each do |key,value|
|
19
|
+
if value.respond_to?(:key)
|
20
|
+
wrapKeys(value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.unwrapKeys(theHash)
|
26
|
+
newKeys = Hash.new
|
27
|
+
theHash.keys.each do |key|
|
28
|
+
if (key.include? '^p') || (key.include? '^c')
|
29
|
+
newKey = key.gsub(/\^p/, '.')
|
30
|
+
newKey.gsub!(/\^c/, '^')
|
31
|
+
newKeys[key] = newKey
|
32
|
+
end
|
33
|
+
end
|
34
|
+
newKeys.each { |old, new| theHash[new] = theHash.delete old}
|
35
|
+
# now recurse on any contained hashes
|
36
|
+
theHash.each do |key,value|
|
37
|
+
if value.respond_to?(:key)
|
38
|
+
unwrapKeys(value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
class CqlMeasure
|
2
|
+
include Mongoid::Document
|
3
|
+
include Mongoid::Timestamps
|
4
|
+
include Mongoid::Attributes::Dynamic
|
5
|
+
|
6
|
+
DEFAULT_EFFECTIVE_DATE = Time.gm(2012,12,31,23,59).to_i
|
7
|
+
MP_START_DATE = Time.gm(2012,1,1,0,0).to_i
|
8
|
+
TYPES = ["ep", "eh"]
|
9
|
+
|
10
|
+
field :id, type: String
|
11
|
+
field :measure_id, type: String
|
12
|
+
field :hqmf_id, type: String
|
13
|
+
field :hqmf_set_id, type: String
|
14
|
+
field :hqmf_version_number, type: Integer
|
15
|
+
field :cms_id, type: String
|
16
|
+
field :title, type: String, default: ""
|
17
|
+
field :description, type: String, default: ""
|
18
|
+
field :type, type: String
|
19
|
+
field :category, type: String, default: 'uncategorized'
|
20
|
+
|
21
|
+
field :episode_of_care, type: Boolean
|
22
|
+
field :continuous_variable, type: Boolean
|
23
|
+
field :episode_ids, type: Array
|
24
|
+
|
25
|
+
field :needs_finalize, type: Boolean, default: false
|
26
|
+
|
27
|
+
field :published, type: Boolean
|
28
|
+
field :publish_date, type: Date
|
29
|
+
field :version, type: Integer
|
30
|
+
|
31
|
+
field :elm_annotations, type: Hash
|
32
|
+
|
33
|
+
field :cql, type: Array
|
34
|
+
field :elm, type: Array
|
35
|
+
field :main_cql_library, type: String
|
36
|
+
field :cql_statement_dependencies, type: Hash
|
37
|
+
|
38
|
+
field :population_criteria, type: Hash
|
39
|
+
field :data_criteria, type: Hash
|
40
|
+
field :source_data_criteria, type: Hash
|
41
|
+
field :measure_period, type: Hash
|
42
|
+
field :measure_attributes, type: Array
|
43
|
+
field :populations, type: Array
|
44
|
+
field :populations_cql_map, type: Hash
|
45
|
+
field :observations, type: Array
|
46
|
+
|
47
|
+
field :value_set_oids, type: Array, default: []
|
48
|
+
field :value_set_oid_version_objects, type: Array, default: []
|
49
|
+
|
50
|
+
field :complexity, type: Hash
|
51
|
+
|
52
|
+
belongs_to :user
|
53
|
+
belongs_to :bundle, class_name: "HealthDataStandards::CQM::Bundle"
|
54
|
+
has_and_belongs_to_many :records, :inverse_of => nil
|
55
|
+
has_one :package, class_name: "CqlMeasurePackage", inverse_of: :measure, dependent: :delete
|
56
|
+
|
57
|
+
scope :by_measure_id, ->(id) { where({'measure_id'=>id }) }
|
58
|
+
scope :by_type, ->(type) { where({'type'=>type}) }
|
59
|
+
scope :by_user, ->(user) { where user_id: user.id }
|
60
|
+
|
61
|
+
index "user_id" => 1
|
62
|
+
# Find the measures matching a patient
|
63
|
+
def self.for_patient(record)
|
64
|
+
where user_id: record.user_id, hqmf_set_id: { '$in' => record.measure_ids }
|
65
|
+
end
|
66
|
+
|
67
|
+
def value_sets
|
68
|
+
options = { oid: value_set_oids }
|
69
|
+
options[:user_id] = user.id if user?
|
70
|
+
@value_sets ||= HealthDataStandards::SVS::ValueSet.in(options)
|
71
|
+
@value_sets
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the hqmf-parser's ruby implementation of an HQMF document.
|
75
|
+
# Rebuild from population_criteria, data_criteria, and measure_period JSON
|
76
|
+
def as_hqmf_model
|
77
|
+
json = {
|
78
|
+
"id" => self.measure_id,
|
79
|
+
"title" => self.title,
|
80
|
+
"description" => self.description,
|
81
|
+
"population_criteria" => self.population_criteria,
|
82
|
+
"data_criteria" => self.data_criteria,
|
83
|
+
"source_data_criteria" => self.source_data_criteria,
|
84
|
+
"measure_period" => self.measure_period,
|
85
|
+
"attributes" => self.measure_attributes,
|
86
|
+
"populations" => self.populations,
|
87
|
+
"hqmf_id" => self.hqmf_id,
|
88
|
+
"hqmf_set_id" => self.hqmf_set_id,
|
89
|
+
"hqmf_version_number" => self.hqmf_version_number,
|
90
|
+
"cms_id" => self.cms_id
|
91
|
+
}
|
92
|
+
HQMF::Document.from_json(json)
|
93
|
+
end
|
94
|
+
|
95
|
+
def all_data_criteria
|
96
|
+
as_hqmf_model.all_data_criteria
|
97
|
+
end
|
98
|
+
|
99
|
+
# Note whether or not the measure is a continuous variable measure.
|
100
|
+
before_save :set_continuous_variable
|
101
|
+
def set_continuous_variable
|
102
|
+
# The return value of this function is not related to whether or not this
|
103
|
+
# measure is a CV measure. The true return value ensures false is not
|
104
|
+
# accidentally returned here, which would cause the chain of 'before_*' to
|
105
|
+
# stop executing.
|
106
|
+
self.continuous_variable = populations.map {|x| x.keys}.flatten.uniq.include? HQMF::PopulationCriteria::MSRPOPL
|
107
|
+
true
|
108
|
+
end
|
109
|
+
|
110
|
+
# When saving calculate the cyclomatic complexity of the measure
|
111
|
+
# TODO: Do we want to consider a measure other than "cyclomatic complexity" for CQL?
|
112
|
+
# TODO: THIS IS NOT CYCLOMATIC COMPLEXITY, ALL MULTIPLE ELEMENT EXPRESSIONS GET COUNTED AS HIGHER COMPLEXITY, NOT JUST LOGICAL
|
113
|
+
before_save :calculate_complexity
|
114
|
+
def calculate_complexity
|
115
|
+
# We calculate the complexity for each statement, and (at least for now) store the result in the same way
|
116
|
+
# we store the complexity for QDM variables
|
117
|
+
# TODO: consider whether this is too much of a force fit
|
118
|
+
self.complexity = { variables: [] }
|
119
|
+
# Recursively look through an expression to count the logical branches
|
120
|
+
def count_expression_logical_branches(expression)
|
121
|
+
case expression
|
122
|
+
when nil
|
123
|
+
0
|
124
|
+
when Array
|
125
|
+
expression.map { |exp| count_expression_logical_branches(exp) }.sum
|
126
|
+
when Hash
|
127
|
+
case expression['type']
|
128
|
+
when 'And', 'Or', 'Not'
|
129
|
+
count_expression_logical_branches(expression['operand'])
|
130
|
+
when 'Query'
|
131
|
+
# TODO: Do we need to look into the source side of the query? Can there be logical operators there?
|
132
|
+
count_expression_logical_branches(expression['where']) + count_expression_logical_branches(expression['relationship'])
|
133
|
+
else
|
134
|
+
1
|
135
|
+
end
|
136
|
+
else
|
137
|
+
0
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Determine the complexity of each statement
|
142
|
+
self.elm.each do |elm|
|
143
|
+
if statements = elm.try(:[], 'library').try(:[], 'statements').try(:[], 'def')
|
144
|
+
statements.each do |statement|
|
145
|
+
self.complexity[:variables] << { name: statement['name'], complexity: count_expression_logical_branches(statement['expression']) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
self.complexity
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
class CqlMeasurePackage
|
155
|
+
include Mongoid::Document
|
156
|
+
include Mongoid::Timestamps
|
157
|
+
|
158
|
+
field :file, type: BSON::Binary
|
159
|
+
belongs_to :measure, class_name: "CqlMeasure", inverse_of: :package
|
160
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
class Measure
|
2
|
+
include Mongoid::Document
|
3
|
+
include Mongoid::Timestamps
|
4
|
+
include Mongoid::Attributes::Dynamic
|
5
|
+
|
6
|
+
DEFAULT_EFFECTIVE_DATE = Time.gm(2012,12,31,23,59).to_i
|
7
|
+
MP_START_DATE = Time.gm(2012,1,1,0,0).to_i
|
8
|
+
TYPES = ["ep", "eh"]
|
9
|
+
|
10
|
+
store_in collection: 'draft_measures'
|
11
|
+
|
12
|
+
field :id, type: String
|
13
|
+
field :measure_id, type: String
|
14
|
+
field :hqmf_id, type: String # should be using this one as primary id!!
|
15
|
+
field :hqmf_set_id, type: String
|
16
|
+
field :hqmf_version_number, type: Integer
|
17
|
+
field :cms_id, type: String
|
18
|
+
field :title, type: String
|
19
|
+
field :description, type: String
|
20
|
+
field :type, type: String
|
21
|
+
field :category, type: String, default: 'uncategorized'
|
22
|
+
|
23
|
+
field :episode_of_care, type: Boolean
|
24
|
+
field :continuous_variable, type: Boolean
|
25
|
+
field :episode_ids, type: Array # of String ids
|
26
|
+
field :custom_functions, type: Hash # stores a custom function for a population criteria (used only in ADE_TTR for observation)
|
27
|
+
field :force_sources, type: Array # stores a list of source data criteria to force method creation for (used only in ADE_TTR for LaboratoryTestResultInr)
|
28
|
+
|
29
|
+
field :needs_finalize, type: Boolean, default: false # if true it indicates that the measure needs to have its episodes or submeasure titles defined
|
30
|
+
|
31
|
+
field :published, type: Boolean
|
32
|
+
field :publish_date, type: Date
|
33
|
+
field :version, type: Integer
|
34
|
+
|
35
|
+
field :population_criteria, type: Hash
|
36
|
+
field :data_criteria, type: Hash, default: {}
|
37
|
+
field :source_data_criteria, type: Hash, default: {}
|
38
|
+
field :measure_period, type: Hash
|
39
|
+
field :measure_attributes, type: Array
|
40
|
+
field :populations, type: Array
|
41
|
+
field :preconditions, type: Hash
|
42
|
+
|
43
|
+
field :value_set_oids, type: Array, default: []
|
44
|
+
|
45
|
+
field :map_fns, type: Array, default: []
|
46
|
+
|
47
|
+
field :complexity, type: Hash
|
48
|
+
field :measure_logic, type: Array
|
49
|
+
|
50
|
+
#make sure that the use has a bundle associated with them
|
51
|
+
before_save :set_continuous_variable
|
52
|
+
|
53
|
+
# Cache the generated JS code, with optional options to manipulate cached result
|
54
|
+
def map_fn(population_index, options = {})
|
55
|
+
options.assert_valid_keys :clear_db_cache, :cache_result_in_db, :check_crosswalk
|
56
|
+
# Defaults are: don't clear the cache, do cache the result in the DB, use user specified crosswalk setting
|
57
|
+
options.reverse_merge! clear_db_cache: false, cache_result_in_db: true, check_crosswalk: !!self.user.try(:crosswalk_enabled)
|
58
|
+
self.map_fns[population_index] = nil if options[:clear_db_cache]
|
59
|
+
self.map_fns[population_index] ||= as_javascript(population_index, options[:check_crosswalk])
|
60
|
+
save if changed? && options[:cache_result_in_db]
|
61
|
+
self.map_fns[population_index]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Generate and cache all the javascript for the measure, optionally clearing the cache first
|
65
|
+
def generate_js(options = {})
|
66
|
+
populations.each_with_index { |p, idx| map_fn(idx, options) }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Clear any cached JavaScript, forcing it to be generated next time it's requested
|
70
|
+
def clear_cached_js
|
71
|
+
self.map_fns.map! { nil }
|
72
|
+
self.save
|
73
|
+
end
|
74
|
+
|
75
|
+
belongs_to :user
|
76
|
+
belongs_to :bundle, class_name: "HealthDataStandards::CQM::Bundle"
|
77
|
+
has_and_belongs_to_many :records, :inverse_of => nil
|
78
|
+
|
79
|
+
scope :by_measure_id, ->(id) { where({'measure_id'=>id }) }
|
80
|
+
scope :by_user, ->(user) { where({'user_id'=>user.id}) }
|
81
|
+
scope :by_type, ->(type) { where({'type'=>type}) }
|
82
|
+
|
83
|
+
index "user_id" => 1
|
84
|
+
# Find the measures matching a patient
|
85
|
+
def self.for_patient(record)
|
86
|
+
where user_id: record.user_id, hqmf_set_id: { '$in' => record.measure_ids }
|
87
|
+
end
|
88
|
+
|
89
|
+
TYPE_MAP = {
|
90
|
+
'problem' => 'conditions',
|
91
|
+
'encounter' => 'encounters',
|
92
|
+
'labresults' => 'results',
|
93
|
+
'procedure' => 'procedures',
|
94
|
+
'medication' => 'medications',
|
95
|
+
'rx' => 'medications',
|
96
|
+
'demographics' => 'characteristic',
|
97
|
+
'derived' => 'derived'
|
98
|
+
}
|
99
|
+
|
100
|
+
# Returns the hqmf-parser's ruby implementation of an HQMF document.
|
101
|
+
# Rebuild from population_criteria, data_criteria, and measure_period JSON
|
102
|
+
def as_hqmf_model
|
103
|
+
json = {
|
104
|
+
"id" => self.measure_id,
|
105
|
+
"title" => self.title,
|
106
|
+
"description" => self.description,
|
107
|
+
"population_criteria" => self.population_criteria,
|
108
|
+
"data_criteria" => self.data_criteria,
|
109
|
+
"source_data_criteria" => self.source_data_criteria,
|
110
|
+
"measure_period" => self.measure_period,
|
111
|
+
"attributes" => self.measure_attributes,
|
112
|
+
"populations" => self.populations,
|
113
|
+
"hqmf_id" => self.hqmf_id,
|
114
|
+
"hqmf_set_id" => self.hqmf_set_id,
|
115
|
+
"hqmf_version_number" => self.hqmf_version_number,
|
116
|
+
"cms_id" => self.cms_id
|
117
|
+
}
|
118
|
+
|
119
|
+
HQMF::Document.from_json(json)
|
120
|
+
end
|
121
|
+
|
122
|
+
def value_sets
|
123
|
+
options = { oid: value_set_oids }
|
124
|
+
options[:user_id] = user.id if user?
|
125
|
+
@value_sets ||= HealthDataStandards::SVS::ValueSet.in(options)
|
126
|
+
@value_sets
|
127
|
+
end
|
128
|
+
|
129
|
+
def all_data_criteria
|
130
|
+
as_hqmf_model.all_data_criteria
|
131
|
+
end
|
132
|
+
|
133
|
+
def as_javascript(population_index, check_crosswalk=false)
|
134
|
+
options = {
|
135
|
+
value_sets: value_sets,
|
136
|
+
episode_ids: episode_ids,
|
137
|
+
continuous_variable: continuous_variable,
|
138
|
+
force_sources: force_sources,
|
139
|
+
custom_functions: custom_functions,
|
140
|
+
check_crosswalk: check_crosswalk
|
141
|
+
}
|
142
|
+
|
143
|
+
HQMF2JS::Generator::Execution.logic(as_hqmf_model, population_index, options)
|
144
|
+
end
|
145
|
+
|
146
|
+
def set_continuous_variable
|
147
|
+
self.continuous_variable = populations.map {|x| x.keys}.flatten.uniq.include? HQMF::PopulationCriteria::MSRPOPL
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
151
|
+
############################## Measure Criteria Keys ##############################
|
152
|
+
|
153
|
+
# Given a data criteria, return the list of all data criteria keys referenced within, either through
|
154
|
+
# children criteria or temporal references; this includes the passed in criteria reference
|
155
|
+
def data_criteria_criteria_keys(criteria_reference)
|
156
|
+
criteria_keys = [criteria_reference]
|
157
|
+
if criteria = self.data_criteria[criteria_reference]
|
158
|
+
if criteria['children_criteria'].present?
|
159
|
+
criteria_keys.concat(criteria['children_criteria'].map { |c| data_criteria_criteria_keys(c) }.flatten)
|
160
|
+
end
|
161
|
+
if criteria['temporal_references'].present?
|
162
|
+
criteria_keys.concat(criteria['temporal_references'].map { |tr| data_criteria_criteria_keys(tr['reference']) }.flatten)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
criteria_keys
|
166
|
+
end
|
167
|
+
|
168
|
+
# Given a precondition, return the list of all data criteria keys referenced within
|
169
|
+
def precondition_criteria_keys(precondition)
|
170
|
+
if precondition['preconditions'] && precondition['preconditions'].size > 0
|
171
|
+
precondition['preconditions'].map { |p| precondition_criteria_keys(p) }.flatten
|
172
|
+
elsif precondition['reference']
|
173
|
+
data_criteria_criteria_keys(precondition['reference'])
|
174
|
+
else
|
175
|
+
[]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Return the list of all data criteria keys in this measure, indexed by population code
|
180
|
+
def criteria_keys_by_population
|
181
|
+
criteria_keys_by_population = {}
|
182
|
+
population_criteria.each do |name, precondition|
|
183
|
+
criteria_keys_by_population[name] = precondition_criteria_keys(precondition).reject { |ck| ck == 'MeasurePeriod' }
|
184
|
+
end
|
185
|
+
criteria_keys_by_population
|
186
|
+
end
|
187
|
+
|
188
|
+
############################## Measure Complexity Analysis ##############################
|
189
|
+
|
190
|
+
def precondition_complexity(precondition)
|
191
|
+
# We want to calculate the number of branching paths; we can do that by simply counting the leaf nodes.
|
192
|
+
# Any children of this particular node can appear either through child preconditions or by reference to a
|
193
|
+
# data criteria. ASSERTION: a precondition can never both have child preconditions and a data criteria.
|
194
|
+
if precondition['preconditions'] && precondition['preconditions'].size > 0
|
195
|
+
precondition['preconditions'].map { |p| precondition_complexity(p) }.sum
|
196
|
+
elsif precondition['reference']
|
197
|
+
data_criteria_complexity(precondition['reference'])
|
198
|
+
else
|
199
|
+
1
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def data_criteria_complexity(criteria_reference, options = {})
|
204
|
+
options.reverse_merge! calculating_variable: false
|
205
|
+
# We want to calculate the number of branching paths, which we can normally do by counting leaf nodes.
|
206
|
+
# This is more complicated for data criteria because, in addition to direct children, the criteria can
|
207
|
+
# also have temporal references, which can themselves branch. Our approach is to calculate an initial
|
208
|
+
# number of leaf nodes through looking at direct children and then seeing if any additional leaves are
|
209
|
+
# added through temporal references. A temporal reference that doesn't branch doesn't add a leaf node.
|
210
|
+
# Finally, this reference may be a variable, in which case we consider this a leaf node *unless* we are
|
211
|
+
# explicitly calculating the complexity of the variable itself
|
212
|
+
if criteria = self.data_criteria[criteria_reference]
|
213
|
+
complexity = if criteria['children_criteria'].present? && (!criteria['variable'] || options[:calculating_variable])
|
214
|
+
criteria['children_criteria'].map { |c| data_criteria_complexity(c) }.sum
|
215
|
+
else
|
216
|
+
1
|
217
|
+
end
|
218
|
+
complexity + if criteria['temporal_references'].present?
|
219
|
+
criteria['temporal_references'].map { |tr| data_criteria_complexity(tr['reference']) - 1 }.sum
|
220
|
+
else
|
221
|
+
0
|
222
|
+
end
|
223
|
+
else
|
224
|
+
1
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Calculate the complexity of the measure based on cyclomatic complexity (which for simple logical
|
229
|
+
# constructs as used to specify measure populations generally means counting clauses); we calculate
|
230
|
+
# the complexity separately for populations and individual variables; this is called when the
|
231
|
+
# measure is saved so that the calculated complexity is cached in the DB
|
232
|
+
before_save :calculate_complexity
|
233
|
+
def calculate_complexity
|
234
|
+
self.complexity = { populations: [], variables: [] }
|
235
|
+
self.population_criteria.each do |name, precondition|
|
236
|
+
complexity = precondition_complexity(precondition)
|
237
|
+
self.complexity[:populations] << { name: name, complexity: complexity }
|
238
|
+
end
|
239
|
+
self.source_data_criteria.each do |reference, criteria|
|
240
|
+
next unless criteria['variable']
|
241
|
+
name = criteria['description']
|
242
|
+
complexity = data_criteria_complexity(reference, calculating_variable: true)
|
243
|
+
self.complexity[:variables] << { name: name, complexity: complexity }
|
244
|
+
end
|
245
|
+
self.complexity
|
246
|
+
end
|
247
|
+
|
248
|
+
#########################################################################################
|
249
|
+
|
250
|
+
############################## Measure Change Analysis ##############################
|
251
|
+
|
252
|
+
# Extract the measure logic text; this is also cached in the DB
|
253
|
+
before_save :extract_measure_logic
|
254
|
+
def extract_measure_logic
|
255
|
+
self.measure_logic = []
|
256
|
+
# There are occasional issues extracting measure logic; while we want to fix them we also don't want logic
|
257
|
+
# extraction issues to hold up loading or updating a measure
|
258
|
+
begin
|
259
|
+
self.measure_logic.concat HQMF::Measure::LogicExtractor.new().population_logic(self)
|
260
|
+
rescue => e
|
261
|
+
self.measure_logic << "Error parsing measure logic: #{e.message}"
|
262
|
+
end
|
263
|
+
self.measure_logic
|
264
|
+
end
|
265
|
+
|
266
|
+
# Compute a simplified diff hash for Complexity Dashboard usage; stored within measure.latest_diff
|
267
|
+
def diff(other)
|
268
|
+
HQMF::Measure::LogicExtractor.get_measure_logic_diff(self,other,true)
|
269
|
+
end
|
270
|
+
|
271
|
+
#########################################################################################
|
272
|
+
|
273
|
+
def measure_json(population_index=0,check_crosswalk=false)
|
274
|
+
options = {
|
275
|
+
value_sets: value_sets,
|
276
|
+
episode_ids: episode_ids,
|
277
|
+
continuous_variable: continuous_variable,
|
278
|
+
force_sources: force_sources,
|
279
|
+
custom_functions: custom_functions,
|
280
|
+
check_crosswalk: check_crosswalk
|
281
|
+
}
|
282
|
+
population_index ||= 0
|
283
|
+
json = {
|
284
|
+
id: self.hqmf_id,
|
285
|
+
nqf_id: self.measure_id,
|
286
|
+
hqmf_id: self.hqmf_id,
|
287
|
+
hqmf_set_id: self.hqmf_set_id,
|
288
|
+
hqmf_version_number: self.hqmf_version_number,
|
289
|
+
cms_id: self.cms_id,
|
290
|
+
name: self.title,
|
291
|
+
description: self.description,
|
292
|
+
type: self.type,
|
293
|
+
category: self.category,
|
294
|
+
map_fn: HQMF2JS::Generator::Execution.measure_js(self.as_hqmf_model, population_index, options),
|
295
|
+
continuous_variable: self.continuous_variable,
|
296
|
+
episode_of_care: self.episode_of_care,
|
297
|
+
hqmf_document: self.as_hqmf_model.to_json
|
298
|
+
}
|
299
|
+
|
300
|
+
if (self.populations.count > 1)
|
301
|
+
sub_ids = ('a'..'az').to_a
|
302
|
+
json[:sub_id] = sub_ids[population_index]
|
303
|
+
population_title = self.populations[population_index]['title']
|
304
|
+
json[:subtitle] = population_title
|
305
|
+
json[:short_subtitle] = population_title
|
306
|
+
end
|
307
|
+
|
308
|
+
if self.continuous_variable
|
309
|
+
observation = self.population_criteria[self.populations[population_index][HQMF::PopulationCriteria::OBSERV]]
|
310
|
+
json[:aggregator] = observation['aggregator']
|
311
|
+
end
|
312
|
+
|
313
|
+
json[:oids] = self.value_sets.map{|value_set| value_set.oid}.uniq
|
314
|
+
|
315
|
+
population_ids = {}
|
316
|
+
HQMF::PopulationCriteria::ALL_POPULATION_CODES.each do |type|
|
317
|
+
population_key = self.populations[population_index][type]
|
318
|
+
population_criteria = self.population_criteria[population_key]
|
319
|
+
if (population_criteria)
|
320
|
+
population_ids[type] = population_criteria['hqmf_id']
|
321
|
+
end
|
322
|
+
end
|
323
|
+
stratification = self['populations'][population_index]['stratification']
|
324
|
+
if stratification
|
325
|
+
population_ids['stratification'] = stratification
|
326
|
+
end
|
327
|
+
json[:population_ids] = population_ids
|
328
|
+
json
|
329
|
+
end
|
330
|
+
end
|