bonnie_bundler 2.0.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.
- 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,178 @@
|
|
|
1
|
+
module Measures
|
|
2
|
+
# Utility class for loading measure definitions into the database
|
|
3
|
+
class Loader
|
|
4
|
+
|
|
5
|
+
SOURCE_PATH = File.join(".", "db", "measures")
|
|
6
|
+
VALUE_SET_PATH = File.join(".", "db", "value_sets")
|
|
7
|
+
HQMF_VS_OID_CACHE = File.join(".", "db", "hqmf_vs_oid_cache")
|
|
8
|
+
PARSERS = [HQMF::Parser::V2CQLParser, HQMF::Parser::V2Parser,HQMF::Parser::V1Parser,SimpleXml::Parser::V1Parser]
|
|
9
|
+
|
|
10
|
+
def self.parse_hqmf_model(xml_path)
|
|
11
|
+
xml_contents = Nokogiri::XML(File.new xml_path)
|
|
12
|
+
parser = get_parser(xml_contents)
|
|
13
|
+
parser.parse(xml_contents)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.load_hqmf_model_json(json, user, measure_oids, measure_details=nil)
|
|
17
|
+
|
|
18
|
+
measure = Measure.new
|
|
19
|
+
measure.user = user if user
|
|
20
|
+
measure.bundle = user.bundle if (user && user.respond_to?(:bundle) )
|
|
21
|
+
# measure.id = json["hqmf_id"]
|
|
22
|
+
measure.measure_id = json["id"]
|
|
23
|
+
measure.hqmf_id = json["hqmf_id"]
|
|
24
|
+
measure.hqmf_set_id = json["hqmf_set_id"]
|
|
25
|
+
measure.hqmf_version_number = json["hqmf_version_number"]
|
|
26
|
+
measure.cms_id = json["cms_id"]
|
|
27
|
+
measure.title = json["title"]
|
|
28
|
+
measure.description = json["description"]
|
|
29
|
+
measure.measure_attributes = json["attributes"]
|
|
30
|
+
measure.populations = json['populations']
|
|
31
|
+
measure.value_set_oids = measure_oids
|
|
32
|
+
|
|
33
|
+
metadata = measure_details
|
|
34
|
+
if metadata
|
|
35
|
+
measure.measure_id = metadata["nqf_id"]
|
|
36
|
+
measure.type = metadata["type"]
|
|
37
|
+
measure.category = metadata["category"]
|
|
38
|
+
measure.episode_of_care = metadata["episode_of_care"]
|
|
39
|
+
measure.continuous_variable = metadata["continuous_variable"]
|
|
40
|
+
measure.episode_ids = metadata["episode_ids"]
|
|
41
|
+
puts "\tWARNING: Episode of care does not align with episode ids existance" if ((!measure.episode_ids.nil? && measure.episode_ids.length > 0) ^ measure.episode_of_care)
|
|
42
|
+
if (measure.populations.count > 1)
|
|
43
|
+
sub_ids = ('a'..'az').to_a
|
|
44
|
+
measure.populations.each_with_index do |population, population_index|
|
|
45
|
+
sub_id = sub_ids[population_index]
|
|
46
|
+
population_title = metadata['subtitles'][sub_id] if metadata['subtitles']
|
|
47
|
+
measure.populations[population_index]['title'] = population_title if population_title
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
measure.custom_functions = metadata["custom_functions"]
|
|
51
|
+
measure.force_sources = metadata["force_sources"]
|
|
52
|
+
else
|
|
53
|
+
measure.type = "unknown"
|
|
54
|
+
measure.category = "Miscellaneous"
|
|
55
|
+
measure.episode_of_care = false
|
|
56
|
+
measure.continuous_variable = false
|
|
57
|
+
puts "\tWARNING: Could not find metadata for measure: #{measure.hqmf_set_id}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
measure.population_criteria = json["population_criteria"]
|
|
61
|
+
measure.data_criteria = json["data_criteria"]
|
|
62
|
+
measure.source_data_criteria = json["source_data_criteria"]
|
|
63
|
+
puts "\tCould not find episode ids #{measure.episode_ids} in measure #{measure.cms_id || measure.measure_id}" if (measure.episode_ids && measure.episode_of_care && (measure.episode_ids - measure.source_data_criteria.keys).length > 0)
|
|
64
|
+
measure.measure_period = json["measure_period"]
|
|
65
|
+
measure
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.load_hqmf_cql_model_json(json, user, measure_oids, main_cql_library, cql_definition_dependency_structure, elm, elm_annotations, cql, measure_details=nil, value_set_oid_version_objects)
|
|
69
|
+
measure = CqlMeasure.new
|
|
70
|
+
measure.user = user if user
|
|
71
|
+
measure.bundle = user.bundle if (user && user.respond_to?(:bundle))
|
|
72
|
+
measure.measure_id = json["id"]
|
|
73
|
+
measure.cql = cql
|
|
74
|
+
measure.elm = elm
|
|
75
|
+
measure.elm_annotations = elm_annotations
|
|
76
|
+
# Add metadata
|
|
77
|
+
measure.hqmf_id = json["hqmf_id"]
|
|
78
|
+
measure.hqmf_set_id = json["hqmf_set_id"]
|
|
79
|
+
measure.hqmf_version_number = json["hqmf_version_number"]
|
|
80
|
+
measure.cms_id = json["cms_id"]
|
|
81
|
+
measure.title = json["title"]
|
|
82
|
+
measure.description = json["description"]
|
|
83
|
+
measure.measure_attributes = json["attributes"]
|
|
84
|
+
measure.populations = json['populations']
|
|
85
|
+
measure.value_set_oids = measure_oids
|
|
86
|
+
measure.value_set_oid_version_objects = value_set_oid_version_objects
|
|
87
|
+
|
|
88
|
+
# Set CQL specific information
|
|
89
|
+
measure.cql = cql
|
|
90
|
+
measure.elm = elm
|
|
91
|
+
measure.main_cql_library = main_cql_library
|
|
92
|
+
measure.cql_statement_dependencies = cql_definition_dependency_structure
|
|
93
|
+
|
|
94
|
+
# Add metadata
|
|
95
|
+
metadata = measure_details
|
|
96
|
+
if metadata
|
|
97
|
+
measure.measure_id = metadata["nqf_id"]
|
|
98
|
+
measure.type = metadata["type"]
|
|
99
|
+
measure.category = metadata["category"]
|
|
100
|
+
measure.episode_of_care = metadata["episode_of_care"]
|
|
101
|
+
measure.continuous_variable = metadata["continuous_variable"]
|
|
102
|
+
measure.episode_ids = metadata["episode_ids"]
|
|
103
|
+
puts "\tWARNING: Episode of care does not align with episode ids existance" if ((!measure.episode_ids.nil? && measure.episode_ids.length > 0) ^ measure.episode_of_care)
|
|
104
|
+
if (measure.populations.count > 1)
|
|
105
|
+
sub_ids = ('a'..'az').to_a
|
|
106
|
+
measure.populations.each_with_index do |population, population_index|
|
|
107
|
+
sub_id = sub_ids[population_index]
|
|
108
|
+
population_title = metadata['subtitles'][sub_id] if metadata['subtitles']
|
|
109
|
+
measure.populations[population_index]['title'] = population_title if population_title
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
measure.type = "unknown"
|
|
114
|
+
measure.category = "Miscellaneous"
|
|
115
|
+
measure.episode_of_care = false
|
|
116
|
+
measure.continuous_variable = false
|
|
117
|
+
puts "\tWARNING: Could not find metadata for measure: #{measure.hqmf_set_id}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Set measure population information
|
|
121
|
+
measure.population_criteria = json["population_criteria"]
|
|
122
|
+
measure.data_criteria = json["data_criteria"]
|
|
123
|
+
measure.source_data_criteria = json["source_data_criteria"]
|
|
124
|
+
puts "\tCould not find episode ids #{measure.episode_ids} in measure #{measure.cms_id || measure.measure_id}" if (measure.episode_ids && measure.episode_of_care && (measure.episode_ids - measure.source_data_criteria.keys).length > 0)
|
|
125
|
+
measure.measure_period = json["measure_period"]
|
|
126
|
+
measure.population_criteria = json["population_criteria"]
|
|
127
|
+
measure.populations_cql_map = json["populations_cql_map"]
|
|
128
|
+
measure.observations = json["observations"]
|
|
129
|
+
|
|
130
|
+
measure
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.save_sources(measure, hqmf_path, html_path, xls_path=nil)
|
|
134
|
+
# Save original files
|
|
135
|
+
if (html_path)
|
|
136
|
+
html_out_path = File.join(SOURCE_PATH, "html")
|
|
137
|
+
FileUtils.mkdir_p html_out_path
|
|
138
|
+
FileUtils.cp(html_path, File.join(html_out_path,"#{measure.hqmf_id}.html"))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if (xls_path)
|
|
142
|
+
value_set_out_path = File.join(SOURCE_PATH, "value_sets")
|
|
143
|
+
FileUtils.mkdir_p value_set_out_path
|
|
144
|
+
FileUtils.cp(xls_path, File.join(value_set_out_path,"#{measure.hqmf_id}.xls"))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
hqmf_out_path = File.join(SOURCE_PATH, "hqmf")
|
|
148
|
+
FileUtils.mkdir_p hqmf_out_path
|
|
149
|
+
FileUtils.cp(hqmf_path, File.join(hqmf_out_path, "#{measure.hqmf_id}.xml"))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.parse(xml_contents)
|
|
153
|
+
doc = xml_contents.kind_of?(Nokogiri::XML::Document) ? xml_contents : Nokogiri::XML(xml_contents)
|
|
154
|
+
doc
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.clear_sources
|
|
158
|
+
FileUtils.rm_r File.join(SOURCE_PATH, "html") if File.exist?(File.join(SOURCE_PATH, "html"))
|
|
159
|
+
FileUtils.rm_r File.join(SOURCE_PATH, "value_sets") if File.exist?(File.join(SOURCE_PATH, "value_sets"))
|
|
160
|
+
FileUtils.rm_r File.join(SOURCE_PATH, "hqmf") if File.exist?(File.join(SOURCE_PATH, "hqmf"))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.parse_measures_yml(measures_yml)
|
|
164
|
+
YAML.load_file(measures_yml)['measures']
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.get_parser(xml_contents)
|
|
168
|
+
doc = self.parse(xml_contents)
|
|
169
|
+
PARSERS.each do |p|
|
|
170
|
+
if p.valid? doc
|
|
171
|
+
return p.new
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
raise "unknown document type"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module Measures
|
|
2
|
+
|
|
3
|
+
# Utility class for loading value sets
|
|
4
|
+
class ValueSetLoader
|
|
5
|
+
|
|
6
|
+
def self.save_value_sets(value_set_models, user = nil)
|
|
7
|
+
#loaded_value_sets = HealthDataStandards::SVS::ValueSet.all.map(&:oid)
|
|
8
|
+
value_set_models.each do |vsm|
|
|
9
|
+
HealthDataStandards::SVS::ValueSet.by_user(user).where(oid: vsm.oid).delete_all()
|
|
10
|
+
vsm.user = user
|
|
11
|
+
#bundle id for user should always be the same 1 user to 1 bundle
|
|
12
|
+
#using this to allow cat I generation without extensive modification to HDS
|
|
13
|
+
vsm.bundle = user.bundle if (user && user.respond_to?(:bundle))
|
|
14
|
+
vsm.save!
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.get_value_set_models(value_set_oids, user=nil)
|
|
19
|
+
HealthDataStandards::SVS::ValueSet.by_user(user).in(oid: value_set_oids)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.load_value_sets_from_vsac(value_sets, username, password, user=nil, overwrite=false, includeDraft=false, ticket_granting_ticket=nil, use_cache=false, measure_id=nil)
|
|
23
|
+
# Get a list of just the oids
|
|
24
|
+
value_set_oids = value_sets.map {|value_set| value_set[:oid]}
|
|
25
|
+
value_set_models = []
|
|
26
|
+
from_vsac = 0
|
|
27
|
+
existing_value_set_map = {}
|
|
28
|
+
begin
|
|
29
|
+
backup_vs = []
|
|
30
|
+
if overwrite
|
|
31
|
+
backup_vs = get_existing_vs(user, value_set_oids).to_a
|
|
32
|
+
delete_existing_vs(user, value_set_oids)
|
|
33
|
+
end
|
|
34
|
+
nlm_config = APP_CONFIG["nlm"]
|
|
35
|
+
|
|
36
|
+
errors = {}
|
|
37
|
+
api = HealthDataStandards::Util::VSApiV2.new(nlm_config["ticket_url"],nlm_config["api_url"],username, password, ticket_granting_ticket)
|
|
38
|
+
|
|
39
|
+
if use_cache
|
|
40
|
+
codeset_base_dir = Measures::Loader::VALUE_SET_PATH
|
|
41
|
+
FileUtils.mkdir_p(codeset_base_dir)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
RestClient.proxy = ENV["http_proxy"]
|
|
45
|
+
value_sets.each do |value_set|
|
|
46
|
+
value_set_version = value_set[:version] ? value_set[:version] : "N/A"
|
|
47
|
+
#When querying vsac via profile, the version is always set to N/A
|
|
48
|
+
#As such, we can set the version to the profile.
|
|
49
|
+
#However, a value_set can have a version and profile that are identical, as such the versions that are profiles are denoted as such.
|
|
50
|
+
value_set_profile = (value_set[:profile] && !includeDraft) ? value_set[:profile] : nlm_config["profile"]
|
|
51
|
+
value_set_profile = "Profile:#{value_set_profile}"
|
|
52
|
+
|
|
53
|
+
query_version = ""
|
|
54
|
+
if includeDraft
|
|
55
|
+
query_version = "Draft-#{measure_id}"
|
|
56
|
+
elsif value_set[:profile]
|
|
57
|
+
query_version = value_set_profile
|
|
58
|
+
else
|
|
59
|
+
query_version = value_set_version
|
|
60
|
+
end
|
|
61
|
+
# only access the database if we don't intend on using cached values
|
|
62
|
+
set = HealthDataStandards::SVS::ValueSet.where({user_id: user.id, oid: value_set[:oid], version: query_version}).first() unless use_cache
|
|
63
|
+
if (includeDraft && set)
|
|
64
|
+
set.delete
|
|
65
|
+
set = nil
|
|
66
|
+
end
|
|
67
|
+
if (set)
|
|
68
|
+
existing_value_set_map[set.oid] = set
|
|
69
|
+
else
|
|
70
|
+
vs_data = nil
|
|
71
|
+
|
|
72
|
+
# try to access the cached result for the value set if it exists.
|
|
73
|
+
cached_service_result = File.join(codeset_base_dir,"#{value_set[:oid]}.xml") if use_cache
|
|
74
|
+
if (cached_service_result && File.exists?(cached_service_result))
|
|
75
|
+
vs_data = File.read cached_service_result
|
|
76
|
+
else
|
|
77
|
+
# If includeDraft is true the latest vs are required, so the latest profile should be used.
|
|
78
|
+
if includeDraft
|
|
79
|
+
vs_data = api.get_valueset(value_set[:oid], include_draft: includeDraft, profile: nlm_config["profile"])
|
|
80
|
+
elsif value_set[:version]
|
|
81
|
+
vs_data = api.get_valueset(value_set[:oid], version: value_set[:version])
|
|
82
|
+
else
|
|
83
|
+
# If no version, call with profile.
|
|
84
|
+
# If a profile is specified, use it. Otherwise, use default.
|
|
85
|
+
profile = value_set[:profile] ? value_set[:profile] : nlm_config["profile"]
|
|
86
|
+
vs_data = api.get_valueset(value_set[:oid], profile: profile)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
vs_data.force_encoding("utf-8") # there are some funky unicodes coming out of the vs response that are not in ASCII as the string reports to be
|
|
90
|
+
from_vsac += 1
|
|
91
|
+
# write all valueset data retrieved if using a cache
|
|
92
|
+
File.open(cached_service_result, 'w') {|f| f.write(vs_data) } if use_cache
|
|
93
|
+
|
|
94
|
+
doc = Nokogiri::XML(vs_data)
|
|
95
|
+
|
|
96
|
+
doc.root.add_namespace_definition("vs","urn:ihe:iti:svs:2008")
|
|
97
|
+
|
|
98
|
+
vs_element = doc.at_xpath("/vs:RetrieveValueSetResponse/vs:ValueSet|/vs:RetrieveMultipleValueSetsResponse/vs:DescribedValueSet")
|
|
99
|
+
|
|
100
|
+
if vs_element && vs_element['ID'] == value_set[:oid]
|
|
101
|
+
vs_element['id'] = value_set[:oid]
|
|
102
|
+
set = HealthDataStandards::SVS::ValueSet.load_from_xml(doc)
|
|
103
|
+
set.user = user
|
|
104
|
+
#bundle id for user should always be the same 1 user to 1 bundle
|
|
105
|
+
#using this to allow cat I generation without extensive modification to HDS
|
|
106
|
+
set.bundle = user.bundle if (user && user.respond_to?(:bundle))
|
|
107
|
+
# As of t9/7/2017, when valuesets are retrieved from VSAC via profile, their version defaults to N/A
|
|
108
|
+
# As such, we set the version to the profile with an indicator.
|
|
109
|
+
set.version = query_version
|
|
110
|
+
set.save!
|
|
111
|
+
existing_value_set_map[set.oid] = set
|
|
112
|
+
else
|
|
113
|
+
raise "Value set not found: #{oid}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
rescue Exception => e
|
|
118
|
+
if (overwrite)
|
|
119
|
+
delete_existing_vs(user, value_set_oids)
|
|
120
|
+
backup_vs.each {|vs| HealthDataStandards::SVS::ValueSet.new(vs.attributes).save }
|
|
121
|
+
end
|
|
122
|
+
raise VSACException.new "#{e.message}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
puts "\tloaded #{from_vsac} value sets from vsac" if from_vsac > 0
|
|
126
|
+
existing_value_set_map.values
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.get_existing_vs(user, value_set_oids)
|
|
130
|
+
HealthDataStandards::SVS::ValueSet.by_user(user).where(oid: {'$in'=>value_set_oids})
|
|
131
|
+
end
|
|
132
|
+
def self.delete_existing_vs(user, value_set_oids)
|
|
133
|
+
get_existing_vs(user, value_set_oids).delete_all()
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
module HQMF
|
|
2
|
+
module Measure
|
|
3
|
+
class LogicExtractor
|
|
4
|
+
|
|
5
|
+
POPULATION_MAP = {
|
|
6
|
+
'STRAT' => 'Stratification',
|
|
7
|
+
'IPP' => 'Initial Patient Population',
|
|
8
|
+
'DENOM' => 'Denominator',
|
|
9
|
+
'NUMER' => 'Numerator',
|
|
10
|
+
'DENEXCEP' => 'Denominator Exceptions',
|
|
11
|
+
'DENEX' => 'Denominator Exclusions',
|
|
12
|
+
'MSRPOPL' => 'Measure Population',
|
|
13
|
+
'OBSERV' => 'Measure Observations'
|
|
14
|
+
}
|
|
15
|
+
AGGREGATOR_MAP = {
|
|
16
|
+
'MEAN' => 'Mean of',
|
|
17
|
+
'MEDIAN' => 'Median of'
|
|
18
|
+
}
|
|
19
|
+
LOGIC_OPERATOR_MAP = { 'XPRODUCT' => 'AND' }
|
|
20
|
+
SET_OPERATOR_MAP = {
|
|
21
|
+
'INTERSECT' => 'Intersection of',
|
|
22
|
+
'UNION' => 'Union of'
|
|
23
|
+
}
|
|
24
|
+
SUBSET_MAP = {
|
|
25
|
+
'COUNT' => 'COUNT',
|
|
26
|
+
'FIRST' => 'FIRST',
|
|
27
|
+
'SECOND' => 'SECOND',
|
|
28
|
+
'THIRD' => 'THIRD',
|
|
29
|
+
'FOURTH' => 'FOURTH',
|
|
30
|
+
'FIFTH' => 'FIFTH',
|
|
31
|
+
'RECENT' => 'MOST RECENT',
|
|
32
|
+
'LAST' => 'LAST',
|
|
33
|
+
'MIN' => 'MIN',
|
|
34
|
+
'MAX' => 'MAX',
|
|
35
|
+
'MEAN' => 'MEAN',
|
|
36
|
+
'MEDIAN' => 'MEDIAN',
|
|
37
|
+
'TIMEDIFF' => 'Difference between times',
|
|
38
|
+
'DATEDIFF' => 'Difference between dates',
|
|
39
|
+
'DATETIMEDIFF' => 'Difference between date/times'
|
|
40
|
+
}
|
|
41
|
+
OPERATOR_MAP = {
|
|
42
|
+
'satisfies_all' => 'SATISFIES ALL',
|
|
43
|
+
'satisfies_any' => 'SATISFIES ANY'
|
|
44
|
+
}
|
|
45
|
+
TIMING_MAP = {
|
|
46
|
+
'DURING' => 'During',
|
|
47
|
+
'OVERLAP' => 'Overlaps',
|
|
48
|
+
'SBS' => 'Starts Before Start of',
|
|
49
|
+
'SAS' => 'Starts After Start of',
|
|
50
|
+
'SBE' => 'Starts Before End of',
|
|
51
|
+
'SAE' => 'Starts After End of',
|
|
52
|
+
'EBS' => 'Ends Before Start of',
|
|
53
|
+
'EAS' => 'Ends After Start of',
|
|
54
|
+
'EBE' => 'Ends Before End of',
|
|
55
|
+
'EAE' => 'Ends After End of',
|
|
56
|
+
'SDU' => 'Starts During',
|
|
57
|
+
'EDU' => 'Ends During',
|
|
58
|
+
'ECW' => 'Ends Concurrent with',
|
|
59
|
+
'SCW' => 'Starts Concurrent with',
|
|
60
|
+
'ECWS' => 'Ends Concurrent with Start of',
|
|
61
|
+
'SCWE' => 'Starts Concurrent with End of',
|
|
62
|
+
'SBCW' => 'Starts Before or Concurrent with',
|
|
63
|
+
'SBCWE' => 'Starts Before or Concurrent with End of',
|
|
64
|
+
'SACW' => 'Starts After or Concurrent with',
|
|
65
|
+
'SACWE' => 'Starts After or Concurrent with End of',
|
|
66
|
+
'SBDU' => 'Starts Before or During',
|
|
67
|
+
'EBCW' => 'Ends Before or Concurrent with',
|
|
68
|
+
'EBCWS' => 'Ends Before or Concurrent with Start of',
|
|
69
|
+
'EACW' => 'Ends After or Concurrent with',
|
|
70
|
+
'EACWS' => 'Ends After or Concurrent with Start of',
|
|
71
|
+
'EADU' => 'Ends After or During',
|
|
72
|
+
'CONCURRENT' => 'Concurrent with'
|
|
73
|
+
}
|
|
74
|
+
UNIT_MAP = {
|
|
75
|
+
'a' => 'year',
|
|
76
|
+
'mo' => 'month',
|
|
77
|
+
'wk' => 'week',
|
|
78
|
+
'd' => 'day',
|
|
79
|
+
'h' => 'hour',
|
|
80
|
+
'min' => 'minute',
|
|
81
|
+
's' => 'second'
|
|
82
|
+
}
|
|
83
|
+
CONJUNCTION_MAP = {
|
|
84
|
+
'allTrue' => 'AND',
|
|
85
|
+
'atLeastOneTrue' => 'OR'
|
|
86
|
+
}
|
|
87
|
+
FLIP_CONJUNCTION_MAP = {
|
|
88
|
+
'AND' => 'OR',
|
|
89
|
+
'OR' => 'AND'
|
|
90
|
+
}
|
|
91
|
+
SATISFIES_DEFINITIONS = ['satisfies_all','satisfies_any']
|
|
92
|
+
INTERVAL_DEFINITIONS = ['IVL_PQ', 'IVL_TS']
|
|
93
|
+
INTERVAL_TYPES_DEFINITIONS = ['PQ', 'TS']
|
|
94
|
+
|
|
95
|
+
def precondition_logic(precondition, parent_precondition=nil, parent_negation=false, indent=nil)
|
|
96
|
+
results = []
|
|
97
|
+
precondition_key = "precondition_#{precondition['id']}"
|
|
98
|
+
parent_preocondition_key = "precondition_#{parent_precondition['id']}"
|
|
99
|
+
conjunction = translate_conjunction(parent_precondition['conjunction_code'])
|
|
100
|
+
suppress = true if precondition['negation'] && precondition['preconditions'] && precondition['preconditions'].length == 1
|
|
101
|
+
conjunction = FLIP_CONJUNCTION_MAP[conjunction] if parent_negation
|
|
102
|
+
comments = precondition['comments'] || []
|
|
103
|
+
if precondition['reference']
|
|
104
|
+
data_criteria = @measure['data_criteria'][precondition['reference']]
|
|
105
|
+
comments.concat data_criteria['comments'] || []
|
|
106
|
+
end
|
|
107
|
+
indent ||= ""
|
|
108
|
+
indent += "\t"
|
|
109
|
+
|
|
110
|
+
line = ""
|
|
111
|
+
unless suppress
|
|
112
|
+
if comments
|
|
113
|
+
results.concat comments
|
|
114
|
+
end
|
|
115
|
+
line << "#{indent}#{conjunction}"
|
|
116
|
+
line << " NOT" if parent_negation
|
|
117
|
+
line << ":"
|
|
118
|
+
results << line
|
|
119
|
+
end
|
|
120
|
+
if precondition['preconditions']
|
|
121
|
+
results.last << "\n" unless results.blank?
|
|
122
|
+
precondition['preconditions'].each do |p|
|
|
123
|
+
results.concat precondition_logic(p, precondition, precondition['negation'], indent)
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
results.last << " "
|
|
127
|
+
results.concat data_criteria_logic(precondition['reference'])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
results
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def subset_operator_logic(subset_operator)
|
|
134
|
+
results = []
|
|
135
|
+
line = "#{translate_subset(subset_operator['type'])}"
|
|
136
|
+
if subset_operator['value']
|
|
137
|
+
unless subset_operator['value']['type'].to_s == 'ANYNonNull'
|
|
138
|
+
line << "#{value_logic(subset_operator['value'])[0]}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
line << ": "
|
|
142
|
+
results << line
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def value_logic(value, range_comparison=nil)
|
|
146
|
+
results = []
|
|
147
|
+
|
|
148
|
+
is_range = INTERVAL_DEFINITIONS.include?(value['type'])
|
|
149
|
+
is_equivalent = is_range && value['high'] && value['low'] && (value['high']['value'] == value['low']['value']) && value['high']['inclusive?'] && value['low']['inclusive?']
|
|
150
|
+
is_value = INTERVAL_TYPES_DEFINITIONS.include?(value['type'])
|
|
151
|
+
is_any_non_null = value['type'].to_s == 'ANYNonNull'
|
|
152
|
+
is_ts = value['type'].to_s == 'TS'
|
|
153
|
+
|
|
154
|
+
line = ""
|
|
155
|
+
unless is_any_non_null
|
|
156
|
+
if is_value
|
|
157
|
+
line << "#{range_comparison || ''}"
|
|
158
|
+
line << "=" if value['inclusive?']
|
|
159
|
+
if is_ts
|
|
160
|
+
line << translate_date(value['value'])
|
|
161
|
+
else
|
|
162
|
+
line << " #{value['value']}"
|
|
163
|
+
end
|
|
164
|
+
line << " #{translate_unit(value['unit'], value['value'])}"
|
|
165
|
+
else
|
|
166
|
+
if is_range
|
|
167
|
+
if value['high'] && value['low']
|
|
168
|
+
if is_equivalent
|
|
169
|
+
line = value_logic(value['low'])[0]
|
|
170
|
+
else
|
|
171
|
+
line = "#{value_logic(value['low'], '>')[0]} and #{value_logic(value['high'], '<')[0]}"
|
|
172
|
+
end
|
|
173
|
+
else
|
|
174
|
+
if value['high']
|
|
175
|
+
line = " #{value_logic(value['high'], '<')[0]}"
|
|
176
|
+
else
|
|
177
|
+
if value['low']
|
|
178
|
+
line = " #{value_logic(value['low'], '>')[0]}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
else
|
|
183
|
+
line << ": #{translate_oid(value['code_list_id'])}" if value['type'].to_s == 'CD'
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
results << line
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def satisfies_logic(reference, indent=nil)
|
|
191
|
+
results = []
|
|
192
|
+
indent ||= ""
|
|
193
|
+
|
|
194
|
+
data_criteria = @measure.data_criteria[reference]
|
|
195
|
+
root_criteria = @measure.data_criteria[data_criteria['children_criteria'][0]]
|
|
196
|
+
|
|
197
|
+
line = ""
|
|
198
|
+
line << "Occurrence #{root_criteria['specific_occurrence']}:" if root_criteria['specific_occurrence']
|
|
199
|
+
line << "$" if root_criteria['variable']
|
|
200
|
+
|
|
201
|
+
line << "#{root_criteria['description']} #{translate_operator(data_criteria['definition'])}\n"
|
|
202
|
+
results << line
|
|
203
|
+
data_criteria['children_criteria'].each do |cc|
|
|
204
|
+
results << "#{data_criteria_logic(cc, false, true, indent+"\t").join('')}"
|
|
205
|
+
end
|
|
206
|
+
if data_criteria['temporal_references']
|
|
207
|
+
data_criteria['temporal_references'].each do |tr|
|
|
208
|
+
results << "#{indent+"\t"}#{temporal_reference_logic(tr).join()}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
results
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def temporal_reference_logic(temporal_reference)
|
|
216
|
+
results = []
|
|
217
|
+
|
|
218
|
+
line = ""
|
|
219
|
+
line << "#{value_logic(temporal_reference['range'])[0]}" if temporal_reference['range']
|
|
220
|
+
line << " #{translate_timing(temporal_reference['type'])} "
|
|
221
|
+
if temporal_reference['reference'].to_s == "MeasurePeriod"
|
|
222
|
+
line << "\"Measurement Period\""
|
|
223
|
+
else
|
|
224
|
+
line << "#{data_criteria_logic(temporal_reference['reference']).join()}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
results << line
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def data_criteria_logic(reference, expand_variable=false, hide_title=nil, indent=nil)
|
|
231
|
+
results = []
|
|
232
|
+
indent ||= ""
|
|
233
|
+
|
|
234
|
+
data_criteria = @measure.data_criteria[reference]
|
|
235
|
+
unless data_criteria
|
|
236
|
+
data_criteria = @measure.source_data_criteria[reference]
|
|
237
|
+
end
|
|
238
|
+
data_criteria['key'] ||= reference
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if !data_criteria['field_values'].blank?
|
|
242
|
+
data_criteria['field_values'].each do |key, field|
|
|
243
|
+
if field.blank?
|
|
244
|
+
field = {}
|
|
245
|
+
data_criteria['field_values'][key] = field
|
|
246
|
+
end
|
|
247
|
+
field['key'] = key
|
|
248
|
+
field['key_title'] = translate_field(key)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
# handle field values on data_criteria
|
|
252
|
+
is_satisfies = SATISFIES_DEFINITIONS.include?(data_criteria['definition'])
|
|
253
|
+
is_derived = data_criteria['type'].to_s == 'derived'
|
|
254
|
+
has_children = is_derived && (!data_criteria['variable'] || expand_variable)
|
|
255
|
+
is_set_op = SET_OPERATOR_MAP.keys.include?(data_criteria['derivation_operator'])
|
|
256
|
+
|
|
257
|
+
if data_criteria['subset_operators']
|
|
258
|
+
data_criteria['subset_operators'].each do |so|
|
|
259
|
+
results.concat subset_operator_logic(so)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
if has_children
|
|
264
|
+
if is_satisfies
|
|
265
|
+
results.concat satisfies_logic(data_criteria['key'], indent+"\t")
|
|
266
|
+
else
|
|
267
|
+
if data_criteria['children_criteria']
|
|
268
|
+
if is_set_op
|
|
269
|
+
unless expand_variable
|
|
270
|
+
results << "\n#{indent+"\t\t"}#{translate_set_operator(data_criteria['derivation_operator'])}:"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
line = "#{indent}"
|
|
274
|
+
data_criteria['children_criteria'].each_with_index do |cc, cc_ind|
|
|
275
|
+
unless is_set_op
|
|
276
|
+
line << "#{translate_logic_operator(data_criteria['derivation_operator'])} : "
|
|
277
|
+
end
|
|
278
|
+
data_criteria_logic(cc, false, nil, indent+"\t").each_with_index do |dc, dc_ind|
|
|
279
|
+
results << "#{line}\t#{dc}"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
if data_criteria['temporal_references']
|
|
283
|
+
data_criteria['temporal_references'].each do |tr|
|
|
284
|
+
results << "#{indent+"\t\t"}#{temporal_reference_logic(tr).first}"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
line = "#{indent}"
|
|
291
|
+
line = "" if hide_title && !results.blank?
|
|
292
|
+
unless hide_title
|
|
293
|
+
if data_criteria['specific_occurrence']
|
|
294
|
+
line << "Occurrence #{data_criteria['specific_occurrence']}: "
|
|
295
|
+
end
|
|
296
|
+
line << "$" if data_criteria['variable']
|
|
297
|
+
line << data_criteria['description']
|
|
298
|
+
end
|
|
299
|
+
if data_criteria['value']
|
|
300
|
+
unless data_criteria['type'].to_s == 'characteristic'
|
|
301
|
+
line << "(result#{value_logic(data_criteria['value'])[0]})"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
if data_criteria['field_values']
|
|
305
|
+
line << " ( "
|
|
306
|
+
data_criteria['field_values'].each do |field, fv|
|
|
307
|
+
line << fv['key_title'] if fv['key_title']
|
|
308
|
+
line << "#{value_logic(fv)[0]}" if fv['type'] != 'ANYNonNull'
|
|
309
|
+
end
|
|
310
|
+
line << " )"
|
|
311
|
+
end
|
|
312
|
+
if data_criteria['negation']
|
|
313
|
+
line << " ( Not Done : #{translate_oid(data_criteria['negation_code_list_id'])} )"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
results << line
|
|
317
|
+
if data_criteria['temporal_references']
|
|
318
|
+
if data_criteria['temporal_references'].length > 1
|
|
319
|
+
data_criteria['temporal_references'].each do |tr|
|
|
320
|
+
results << "#{indent}#{temporal_reference_logic(tr)}"
|
|
321
|
+
end
|
|
322
|
+
else
|
|
323
|
+
data_criteria['temporal_references'].each do |tr|
|
|
324
|
+
results << temporal_reference_logic(tr).first
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
results.last << "\n" unless results.last.end_with?("\n")
|
|
330
|
+
|
|
331
|
+
results
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def population_criteria_logic(population)
|
|
335
|
+
results = []
|
|
336
|
+
|
|
337
|
+
root_precondition = population['preconditions'][0] if population['preconditions']
|
|
338
|
+
aggregator = population['aggregator']
|
|
339
|
+
( comments = root_precondition.try(:[],'comments') || [] ) | ( population['comments'] || [] )
|
|
340
|
+
results.concat comments if comments
|
|
341
|
+
unless root_precondition.blank?
|
|
342
|
+
if root_precondition['preconditions']
|
|
343
|
+
root_precondition['preconditions'].each do |precondition|
|
|
344
|
+
results.concat precondition_logic(precondition, root_precondition, root_precondition['negation'] || false)
|
|
345
|
+
end
|
|
346
|
+
else
|
|
347
|
+
unless aggregator.blank?
|
|
348
|
+
results << "\t#{translate_aggregator(aggregator)}\n"
|
|
349
|
+
end
|
|
350
|
+
results.concat data_criteria_logic(root_precondition['reference'])
|
|
351
|
+
end
|
|
352
|
+
else
|
|
353
|
+
results << "\tNone\n"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
results
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def population_logic(measure)
|
|
360
|
+
results = []
|
|
361
|
+
@measure = measure
|
|
362
|
+
populations = @measure.population_criteria.keys
|
|
363
|
+
|
|
364
|
+
populations.each do |population|
|
|
365
|
+
population_results = {:code => population, :lines => []}
|
|
366
|
+
population_results[:lines] << "\n#{translate_population(population)}\n"
|
|
367
|
+
population_results[:lines].concat population_criteria_logic(@measure.population_criteria[population])
|
|
368
|
+
results << population_results
|
|
369
|
+
end
|
|
370
|
+
variables_text = variables_logic
|
|
371
|
+
unless variables_text.blank?
|
|
372
|
+
variable_results = {:code => "VARIABLES", :lines => variables_text }
|
|
373
|
+
results << variable_results
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
results
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def variables_logic
|
|
380
|
+
results = []
|
|
381
|
+
|
|
382
|
+
variables = @measure['source_data_criteria'].select{ |key, attrs| attrs['variable'] == true }
|
|
383
|
+
has_variables = variables.length > 0
|
|
384
|
+
|
|
385
|
+
if has_variables
|
|
386
|
+
results << "\nVariables\n"
|
|
387
|
+
variables.each do |title, v|
|
|
388
|
+
results << "\t$#{v['description']} = \n"
|
|
389
|
+
results.concat data_criteria_logic(v['source_data_criteria'], true, nil, "\t")
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
results
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def translate_population(code)
|
|
397
|
+
if match = code.match(/(.*)_(\d+)/)
|
|
398
|
+
"#{POPULATION_MAP[match[1]]} #{match[2].to_i + 1}"
|
|
399
|
+
else
|
|
400
|
+
POPULATION_MAP[code]
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def translate_aggregator(code)
|
|
405
|
+
AGGREGATOR_MAP[code]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def translate_logic_operator(conjunction)
|
|
409
|
+
LOGIC_OPERATOR_MAP[conjunction]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def translate_set_operator(conjunction)
|
|
413
|
+
SET_OPERATOR_MAP[conjunction]
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def translate_field(field_key)
|
|
417
|
+
HQMF::DataCriteria::FIELDS[field_key][:title]
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def translate_subset(subset)
|
|
421
|
+
SUBSET_MAP[subset]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def translate_operator(definition)
|
|
425
|
+
OPERATOR_MAP[definition]
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def translate_timing(code)
|
|
429
|
+
TIMING_MAP[code].downcase
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def translate_unit(unit, value)
|
|
433
|
+
if UNIT_MAP[unit]
|
|
434
|
+
UNIT_MAP[unit] + ( value.to_i > 1 ? 's' : '' )
|
|
435
|
+
else
|
|
436
|
+
unit
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def translate_oid(oid)
|
|
441
|
+
begin
|
|
442
|
+
@measure.value_sets.where({:oid => oid}).first.display_name
|
|
443
|
+
rescue
|
|
444
|
+
oid
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def translate_date(date)
|
|
449
|
+
date
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def translate_conjunction(conjunction)
|
|
453
|
+
CONJUNCTION_MAP[conjunction]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
### Diff methods ###
|
|
457
|
+
|
|
458
|
+
def self.get_measure_logic_text(measure, by_population=false)
|
|
459
|
+
return '' if measure.measure_logic.blank?
|
|
460
|
+
unless by_population
|
|
461
|
+
lines = ''
|
|
462
|
+
measure.measure_logic.each do |population|
|
|
463
|
+
population[:lines].each do |line|
|
|
464
|
+
lines << "#{line}#{"\n" unless line.ends_with?("\n")}"
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
lines
|
|
468
|
+
else
|
|
469
|
+
measure_logic_text = []
|
|
470
|
+
measure.measure_logic.each do |population|
|
|
471
|
+
measure_logic = {:code => population[:code], :lines => []}
|
|
472
|
+
lines = ''
|
|
473
|
+
population[:lines].each do |line|
|
|
474
|
+
lines << "#{line}#{"\n" unless line.ends_with?("\n")}"
|
|
475
|
+
end
|
|
476
|
+
measure_logic[:lines] = lines
|
|
477
|
+
measure_logic_text << measure_logic
|
|
478
|
+
end
|
|
479
|
+
measure_logic_text
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def self.get_measure_logic_diff(measure, other, by_population=false)
|
|
484
|
+
return if other.nil?
|
|
485
|
+
measure_totals = {:total => 0, :deletions => 0, :insertions => 0, :unchanged => 0}
|
|
486
|
+
unless by_population
|
|
487
|
+
compute_diff(get_measure_logic_text(measure), get_measure_logic_text(other), measure_totals)
|
|
488
|
+
else
|
|
489
|
+
measure_text = get_measure_logic_text(measure, by_population)
|
|
490
|
+
other_text = get_measure_logic_text(other, by_population)
|
|
491
|
+
verify_populations(measure_text, other_text)
|
|
492
|
+
diff = {:cms_id => measure.cms_id, :populations => [], :totals => {}}
|
|
493
|
+
measure_text.each_with_index do |population, index|
|
|
494
|
+
population_totals = {:total => 0, :deletions => 0, :insertions => 0, :unchanged => 0}
|
|
495
|
+
first = population[:lines]
|
|
496
|
+
second = other_text.at(index)[:lines]
|
|
497
|
+
diff[:populations] << compute_diff(first, second, population_totals, population[:code])
|
|
498
|
+
measure_totals[:total] += population_totals[:total]
|
|
499
|
+
measure_totals[:deletions] += population_totals[:deletions]
|
|
500
|
+
measure_totals[:insertions] += population_totals[:insertions]
|
|
501
|
+
measure_totals[:unchanged] += population_totals[:unchanged]
|
|
502
|
+
end
|
|
503
|
+
diff[:totals] = measure_totals
|
|
504
|
+
diff
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
private
|
|
509
|
+
|
|
510
|
+
def self.compute_diff(text1, text2, totals, code='test')
|
|
511
|
+
diffs = Diffy::Diff.new(text1, text2, :include_plus_and_minus_in_html => true, :allow_empty_diff => false)
|
|
512
|
+
# File.write("#{code}.html", "<html>\n<style>#{Diffy::CSS}</style>\n#{diffs.to_s(:html)}\n</html>")
|
|
513
|
+
results = {:code => code, :lines => []}
|
|
514
|
+
diffs.each_with_index do |line, ind|
|
|
515
|
+
case line
|
|
516
|
+
when /^\+/
|
|
517
|
+
totals[:insertions] += 1
|
|
518
|
+
results[:lines] << :ins
|
|
519
|
+
when /^-/
|
|
520
|
+
totals[:deletions] += 1
|
|
521
|
+
results[:lines] << :del
|
|
522
|
+
else
|
|
523
|
+
totals[:unchanged] += 1
|
|
524
|
+
results[:lines] << :unchanged
|
|
525
|
+
end
|
|
526
|
+
totals[:total] += 1
|
|
527
|
+
end
|
|
528
|
+
results.merge! totals
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Make sure we have the same populations in the same order for both measures
|
|
532
|
+
def self.verify_populations(measure_text, other_text)
|
|
533
|
+
measure_codes = measure_text.map { |p| p[:code] }
|
|
534
|
+
other_codes = other_text.map { |p| p[:code] }
|
|
535
|
+
# First add any missing codes to each
|
|
536
|
+
(measure_codes - other_codes).each { |code| other_text << { code: code, lines: [] } }
|
|
537
|
+
(other_codes - measure_codes).each { |code| measure_text << { code: code, lines: [] } }
|
|
538
|
+
# Then sort each canonically, allowing for NUMER, NUMER_1, etc
|
|
539
|
+
sorter = proc do |p|
|
|
540
|
+
if match = p[:code].match(/(.*)_(\d+)/)
|
|
541
|
+
[HQMF::PopulationCriteria::ALL_POPULATION_CODES.index(match[1]) || Float::INFINITY, match[2].to_i]
|
|
542
|
+
else
|
|
543
|
+
[HQMF::PopulationCriteria::ALL_POPULATION_CODES.index(p[:code]) || Float::INFINITY, 0]
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
measure_text.sort_by!(&sorter)
|
|
547
|
+
other_text.sort_by!(&sorter)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
end
|