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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +12 -0
  4. data/Gemfile +29 -0
  5. data/Gemfile.lock +267 -0
  6. data/README.md +4 -0
  7. data/Rakefile +29 -0
  8. data/bonnie-bundler.gemspec +29 -0
  9. data/config/initializers/mongo.rb +1 -0
  10. data/config/measures/measures_2_4_0.yml +719 -0
  11. data/config/mongoid.yml +6 -0
  12. data/lib/bonnie_bundler.rb +39 -0
  13. data/lib/ext/hash.rb +28 -0
  14. data/lib/ext/railtie.rb +11 -0
  15. data/lib/ext/valueset.rb +11 -0
  16. data/lib/measures/cql_to_elm_helper.rb +90 -0
  17. data/lib/measures/elm_parser.rb +74 -0
  18. data/lib/measures/loading/base_loader_definition.rb +61 -0
  19. data/lib/measures/loading/cql_loader.rb +420 -0
  20. data/lib/measures/loading/exceptions.rb +10 -0
  21. data/lib/measures/loading/loader.rb +178 -0
  22. data/lib/measures/loading/value_set_loader.rb +137 -0
  23. data/lib/measures/logic_extractor.rb +552 -0
  24. data/lib/measures/mongo_hash_key_wrapper.rb +44 -0
  25. data/lib/models/cql_measure.rb +160 -0
  26. data/lib/models/measure.rb +330 -0
  27. data/test/fixtures/BCS_v5_0_Artifacts.zip +0 -0
  28. data/test/fixtures/CMS158_v5_4_Artifacts.zip +0 -0
  29. data/test/fixtures/CMS158_v5_4_Artifacts_Update.zip +0 -0
  30. data/test/fixtures/DRAFT_CMS2_CQL.zip +0 -0
  31. data/test/fixtures/bonnienesting01_fixed.zip +0 -0
  32. data/test/fixtures/vcr_cassettes/mat_5-4_cql_export_vsac_response.yml +4723 -0
  33. data/test/fixtures/vcr_cassettes/multi_library_webcalls.yml +1892 -0
  34. data/test/fixtures/vcr_cassettes/valid_translation_response.yml +1120 -0
  35. data/test/fixtures/vcr_cassettes/valid_vsac_response.yml +1678 -0
  36. data/test/fixtures/vcr_cassettes/valid_vsac_response_158.yml +1670 -0
  37. data/test/fixtures/vcr_cassettes/valid_vsac_response_158_update.yml +1670 -0
  38. data/test/fixtures/vcr_cassettes/valid_vsac_response_includes_draft.yml +3480 -0
  39. data/test/fixtures/vcr_cassettes/vs_loading_draft_no_profile_version.yml +1198 -0
  40. data/test/fixtures/vcr_cassettes/vs_loading_draft_profile.yml +1198 -0
  41. data/test/fixtures/vcr_cassettes/vs_loading_draft_verion.yml +1198 -0
  42. data/test/fixtures/vcr_cassettes/vs_loading_no_profile_version.yml +1198 -0
  43. data/test/fixtures/vcr_cassettes/vs_loading_profile.yml +1196 -0
  44. data/test/fixtures/vcr_cassettes/vs_loading_version.yml +20331 -0
  45. data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts.zip +0 -0
  46. data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_Version.zip +0 -0
  47. data/test/fixtures/vs_loading/DocofMeds_v5_1_Artifacts_With_Profiles.zip +0 -0
  48. data/test/simplecov_init.rb +18 -0
  49. data/test/test_helper.rb +44 -0
  50. data/test/unit/load_mat_export_test.rb +181 -0
  51. data/test/unit/measure_complexity_test.rb +32 -0
  52. data/test/unit/measure_diff_test.rb +68 -0
  53. data/test/unit/mongo_hash_key_wrapper_test.rb +247 -0
  54. data/test/unit/storing_mat_export_package_test.rb +45 -0
  55. data/test/unit/value_set_loading_test.rb +109 -0
  56. data/test/vcr_setup.rb +20 -0
  57. metadata +258 -0
@@ -0,0 +1,10 @@
1
+ module Measures
2
+ class ValueSetException < Exception
3
+ end
4
+ class VSACException < Exception
5
+ end
6
+ class HQMFException < Exception
7
+ end
8
+ class MeasureLoadingException < Exception
9
+ end
10
+ end
@@ -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