hqmf-parser 1.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 (49) hide show
  1. data/Gemfile +23 -0
  2. data/README.md +903 -0
  3. data/Rakefile +19 -0
  4. data/VERSION +1 -0
  5. data/lib/hqmf-generator/hqmf-generator.rb +308 -0
  6. data/lib/hqmf-model/attribute.rb +35 -0
  7. data/lib/hqmf-model/data_criteria.rb +322 -0
  8. data/lib/hqmf-model/document.rb +172 -0
  9. data/lib/hqmf-model/population_criteria.rb +90 -0
  10. data/lib/hqmf-model/precondition.rb +85 -0
  11. data/lib/hqmf-model/types.rb +318 -0
  12. data/lib/hqmf-model/utilities.rb +52 -0
  13. data/lib/hqmf-parser.rb +54 -0
  14. data/lib/hqmf-parser/1.0/attribute.rb +68 -0
  15. data/lib/hqmf-parser/1.0/comparison.rb +34 -0
  16. data/lib/hqmf-parser/1.0/data_criteria.rb +105 -0
  17. data/lib/hqmf-parser/1.0/document.rb +209 -0
  18. data/lib/hqmf-parser/1.0/expression.rb +52 -0
  19. data/lib/hqmf-parser/1.0/population_criteria.rb +79 -0
  20. data/lib/hqmf-parser/1.0/precondition.rb +89 -0
  21. data/lib/hqmf-parser/1.0/range.rb +65 -0
  22. data/lib/hqmf-parser/1.0/restriction.rb +157 -0
  23. data/lib/hqmf-parser/1.0/utilities.rb +41 -0
  24. data/lib/hqmf-parser/2.0/data_criteria.rb +319 -0
  25. data/lib/hqmf-parser/2.0/document.rb +165 -0
  26. data/lib/hqmf-parser/2.0/population_criteria.rb +53 -0
  27. data/lib/hqmf-parser/2.0/precondition.rb +44 -0
  28. data/lib/hqmf-parser/2.0/types.rb +223 -0
  29. data/lib/hqmf-parser/2.0/utilities.rb +30 -0
  30. data/lib/hqmf-parser/converter/pass1/data_criteria_converter.rb +254 -0
  31. data/lib/hqmf-parser/converter/pass1/document_converter.rb +183 -0
  32. data/lib/hqmf-parser/converter/pass1/population_criteria_converter.rb +135 -0
  33. data/lib/hqmf-parser/converter/pass1/precondition_converter.rb +164 -0
  34. data/lib/hqmf-parser/converter/pass1/precondition_extractor.rb +159 -0
  35. data/lib/hqmf-parser/converter/pass1/simple_data_criteria.rb +35 -0
  36. data/lib/hqmf-parser/converter/pass1/simple_operator.rb +89 -0
  37. data/lib/hqmf-parser/converter/pass1/simple_population_criteria.rb +10 -0
  38. data/lib/hqmf-parser/converter/pass1/simple_precondition.rb +63 -0
  39. data/lib/hqmf-parser/converter/pass1/simple_restriction.rb +64 -0
  40. data/lib/hqmf-parser/converter/pass2/comparison_converter.rb +91 -0
  41. data/lib/hqmf-parser/converter/pass2/operator_converter.rb +169 -0
  42. data/lib/hqmf-parser/converter/pass3/specific_occurrence_converter.rb +86 -0
  43. data/lib/hqmf-parser/converter/pass3/specific_occurrence_converter_bak.rb +70 -0
  44. data/lib/hqmf-parser/parser.rb +22 -0
  45. data/lib/hqmf-parser/value_sets/value_set_parser.rb +206 -0
  46. data/lib/tasks/coverme.rake +8 -0
  47. data/lib/tasks/hqmf.rake +141 -0
  48. data/lib/tasks/value_sets.rake +23 -0
  49. metadata +159 -0
@@ -0,0 +1,183 @@
1
+ module HQMF
2
+ # Class for converting an HQMF 1.0 representation to an HQMF 2.0 representation
3
+ class DocumentConverter
4
+
5
+ BIRTHTIME_CODE_LIST = {'LOINC'=>['21112-8']}
6
+
7
+ def self.convert(json, codes)
8
+
9
+ title = json[:title]
10
+ description = json[:description]
11
+
12
+ metadata = json[:metadata]
13
+ metadata.keys.each {|key| metadata[key.to_s] = metadata[key]; metadata.delete(key.to_sym)}
14
+
15
+ id = metadata["NQF_ID_NUMBER"][:value] if metadata["NQF_ID_NUMBER"]
16
+ attributes = parse_attributes(metadata)
17
+ hqmf_id = json[:hqmf_id]
18
+ hqmf_set_id = json[:hqmf_set_id]
19
+ hqmf_version_number = json[:hqmf_version_number]
20
+
21
+ measure_period = parse_measure_period(json)
22
+ @data_criteria_converter = DataCriteriaConverter.new(json, measure_period)
23
+
24
+ # source data criteria are the original unmodified v2 data criteria
25
+ source_data_criteria = []
26
+ @data_criteria_converter.v2_data_criteria.each {|criteria| source_data_criteria << criteria}
27
+
28
+ # PASS 1
29
+ @population_criteria_converter = PopulationCriteriaConverter.new(json, @data_criteria_converter)
30
+ population_criteria = @population_criteria_converter.population_criteria
31
+
32
+ # PASS 2
33
+ comparison_converter = HQMF::ComparisonConverter.new(@data_criteria_converter)
34
+ comparison_converter.convert_comparisons(population_criteria)
35
+
36
+ # PASS 3
37
+ # specific_occurrence_converter = HQMF::SpecificOccurrenceConverter.new(@data_criteria_converter)
38
+ # specific_occurrence_converter.convert_specific_occurrences(population_criteria)
39
+
40
+ data_criteria = @data_criteria_converter.final_v2_data_criteria
41
+
42
+ populations = @population_criteria_converter.sub_measures
43
+
44
+ doc = HQMF::Document.new(id, hqmf_id, hqmf_set_id, hqmf_version_number, title, description, population_criteria, data_criteria, source_data_criteria, attributes, measure_period, populations)
45
+
46
+ backfill_patient_characteristics_with_codes(doc, codes)
47
+
48
+ HQMF::DocumentConverter.validate(doc, codes)
49
+
50
+ doc
51
+
52
+ end
53
+
54
+ private
55
+
56
+ def self.parse_attributes(metadata)
57
+ attributes = []
58
+ metadata.keys.each do |key|
59
+ attribute_hash = metadata[key]
60
+ code = attribute_hash[:code]
61
+ value = attribute_hash[:value]
62
+ unit = attribute_hash[:unit]
63
+ name = attribute_hash[:name]
64
+ attributes << HQMF::Attribute.new(key,code,value,unit,name)
65
+ end
66
+ attributes
67
+ end
68
+
69
+
70
+ # patient characteristics data criteria such as GENDER require looking at the codes to determine if the
71
+ # measure is interested in Males or Females. This process is awkward, and thus is done as a separate
72
+ # step after the document has been converted.
73
+ def self.backfill_patient_characteristics_with_codes(doc, codes)
74
+
75
+ [].concat(doc.all_data_criteria).concat(doc.source_data_criteria).each do |data_criteria|
76
+ if (data_criteria.type == :characteristic and !data_criteria.property.nil?)
77
+ if (codes)
78
+ value_set = codes[data_criteria.code_list_id]
79
+ puts "\tno value set for unknown patient characteristic: #{data_criteria.id}" unless value_set
80
+ else
81
+ puts "\tno code set to back fill: #{data_criteria.title}"
82
+ next
83
+ end
84
+
85
+ if (data_criteria.property == :gender)
86
+ key = value_set.keys[0]
87
+ data_criteria.value = HQMF::Coded.new('CD','Administrative Sex',value_set[key].first)
88
+ else
89
+ data_criteria.inline_code_list = value_set
90
+ end
91
+
92
+ elsif (data_criteria.type == :characteristic)
93
+ if (codes)
94
+ value_set = codes[data_criteria.code_list_id]
95
+ if (value_set)
96
+ # this is looking for a birthdate characteristic that is set as a generic characteristic but points to a loinc code set
97
+ if (value_set['LOINC'] and value_set['LOINC'].first == '21112-8')
98
+ data_criteria.definition = 'patient_characteristic_birthdate'
99
+ end
100
+ # this is looking for a gender characteristic that is set as a generic characteristic
101
+ gender_key = (value_set.keys.select {|set| set.start_with? 'HL7'}).first
102
+ if (gender_key and ['M','F'].include? value_set[gender_key].first)
103
+ data_criteria.definition = 'patient_characteristic_gender'
104
+ data_criteria.value = HQMF::Coded.new('CD','Gender',value_set[gender_key].first)
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+ end
111
+ end
112
+
113
+
114
+ def self.parse_measure_period(json)
115
+
116
+ # Create a new HQMF::EffectiveTime
117
+ # @param [Value] low
118
+ # @param [Value] high
119
+ # @param [Value] width
120
+ # ----------
121
+ # Create a new HQMF::Value
122
+ # @param [String] type
123
+ # @param [String] unit
124
+ # @param [String] value
125
+ # @param [String] inclusive
126
+ # @param [String] derived
127
+ # @param [String] expression
128
+
129
+ low = HQMF::Value.new('TS',nil,'20100101',nil, nil, nil)
130
+ high = HQMF::Value.new('TS',nil,'20110101',nil, nil, nil)
131
+ width = HQMF::Value.new('PQ','a','1',nil, nil, nil)
132
+
133
+ # puts ('need to figure out a way to make dates dynamic')
134
+
135
+ HQMF::EffectiveTime.new(low,high,width)
136
+ end
137
+
138
+ def self.validate(document,codes)
139
+ puts "\t(#{document.id})document is nil!!!!!!!!!!!" unless document
140
+ puts "\t(#{document.id})codes are nil!!!!!!!!!!!" unless codes
141
+ return unless document and codes
142
+
143
+ referenced_oids = document.all_data_criteria.map(&:code_list_id).compact.uniq
144
+
145
+ referenced_oids.each do |oid|
146
+ value_set = codes[oid]
147
+ puts "\tDC (#{document.id},#{document.title}): referenced OID could not be found #{oid}" unless value_set
148
+ end
149
+
150
+ oid_values = document.all_data_criteria.select {|dc| dc.value != nil and dc.value.type == 'CD'}
151
+
152
+ if oid_values.size > 0
153
+ referenced_oids = (oid_values.map {|dc| dc.value.code_list_id }).compact.uniq
154
+ referenced_oids.each do |oid|
155
+ value_set = codes[oid]
156
+ puts "\tVALUE (#{document.id},#{document.title}): referenced OID could not be found #{oid}" unless value_set
157
+ end
158
+ end
159
+
160
+
161
+ oid_negation = document.all_data_criteria.select {|dc| dc.negation_code_list_id != nil}
162
+ if oid_negation.size > 0
163
+ referenced_oids = (oid_negation.map {|dc| dc.negation_code_list_id}).compact.uniq
164
+ referenced_oids.each do |oid|
165
+ value_set = codes[oid]
166
+ puts "\tNEGATION (#{document.id},#{document.title}): referenced OID could not be found #{oid}" unless value_set
167
+ end
168
+ end
169
+
170
+ oid_fields = document.all_data_criteria.select {|dc| dc.field_values != nil}
171
+ if oid_fields.size > 0
172
+ referenced_oids = (oid_fields.map{|dc| dc.field_values.map {|key,field| puts "field: #{key} is nil" unless field; field.code_list_id if field != nil and field.type == 'CD'}}).flatten.compact.uniq
173
+ referenced_oids.each do |oid|
174
+ value_set = codes[oid]
175
+ puts "\tFIELDS (#{document.id},#{document.title}): referenced OID could not be found #{oid}" unless value_set
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+
182
+ end
183
+ end
@@ -0,0 +1,135 @@
1
+ module HQMF
2
+ # Class for converting an HQMF 1.0 representation to an HQMF 2.0 representation
3
+ class PopulationCriteriaConverter
4
+
5
+ attr_reader :sub_measures
6
+
7
+ def initialize(doc, data_criteria_converter)
8
+ @doc = doc
9
+ @data_criteria_converter = data_criteria_converter
10
+ @population_criteria_by_id = {}
11
+ @population_criteria_by_key = {}
12
+ @population_reference = {}
13
+ parse()
14
+ build_sub_measures()
15
+ end
16
+
17
+ def population_criteria
18
+ @population_criteria_by_key.values
19
+ end
20
+
21
+ private
22
+
23
+ def build_sub_measures()
24
+ @sub_measures = []
25
+ ipps = @population_criteria_by_id.select {|key, value| value.type == HQMF::PopulationCriteria::IPP}
26
+ denoms = @population_criteria_by_id.select {|key, value| value.type == HQMF::PopulationCriteria::DENOM}
27
+ nums = @population_criteria_by_id.select {|key, value| value.type == HQMF::PopulationCriteria::NUMER}
28
+ excls = @population_criteria_by_id.select {|key, value| value.type == HQMF::PopulationCriteria::DENEX}
29
+ denexcs = @population_criteria_by_id.select {|key, value| value.type == HQMF::PopulationCriteria::EXCEP}
30
+
31
+ if (ipps.size<=1 and denoms.size<=1 and nums.size<=1 and excls.size<=1 and denexcs.size<=1 )
32
+ @sub_measures <<
33
+ {
34
+ HQMF::PopulationCriteria::IPP => HQMF::PopulationCriteria::IPP,
35
+ HQMF::PopulationCriteria::DENOM => HQMF::PopulationCriteria::DENOM,
36
+ HQMF::PopulationCriteria::NUMER => HQMF::PopulationCriteria::NUMER,
37
+ HQMF::PopulationCriteria::EXCEP => HQMF::PopulationCriteria::EXCEP,
38
+ HQMF::PopulationCriteria::DENEX => HQMF::PopulationCriteria::DENEX
39
+ }
40
+ else
41
+
42
+ nums.each do |num_id, num|
43
+ @sub_measures << {HQMF::PopulationCriteria::NUMER => num.id}
44
+ end
45
+ apply_to_submeasures(@sub_measures, HQMF::PopulationCriteria::DENOM, denoms.values)
46
+ apply_to_submeasures(@sub_measures, HQMF::PopulationCriteria::IPP, ipps.values)
47
+ apply_to_submeasures(@sub_measures, HQMF::PopulationCriteria::DENEX, excls.values)
48
+ apply_to_submeasures(@sub_measures, HQMF::PopulationCriteria::EXCEP, denexcs.values)
49
+
50
+ keep = []
51
+ @sub_measures.each do |sub|
52
+
53
+ value = sub
54
+ HQMF::PopulationCriteria::ALL_POPULATION_CODES.each do |type|
55
+ key = sub[type]
56
+ if (key)
57
+ reference_id = @population_reference[key]
58
+ reference = @population_criteria_by_id[reference_id] if reference_id
59
+ if (reference)
60
+ criteria = @population_criteria_by_key[sub[reference.type]]
61
+ value['stratification'] = criteria.stratification_id if criteria.stratification_id
62
+ value = nil if (sub[reference.type] != reference.id and criteria.stratification_id.nil?)
63
+ end
64
+ end
65
+ end
66
+ keep << value if (value)
67
+ end
68
+
69
+ keep.each_with_index do |sub, i|
70
+ sub['title'] = "Population #{i+1}"
71
+ sub['id'] = "Population#{i+1}"
72
+ end
73
+
74
+ @sub_measures = keep
75
+
76
+ end
77
+ end
78
+
79
+ def apply_to_submeasures(subs, type, values)
80
+ new_subs = []
81
+ subs.each do |sub|
82
+ values.each do |value|
83
+ if (sub[type] and sub[type] != value.id)
84
+ tmp = {}
85
+ HQMF::PopulationCriteria::ALL_POPULATION_CODES.each do |key|
86
+ tmp[key] = sub[key] if sub[key]
87
+ end
88
+ sub = tmp
89
+ new_subs << sub
90
+ end
91
+ sub[type] = value.id
92
+ end
93
+ end
94
+ subs.concat(new_subs)
95
+ end
96
+
97
+ def find_sub_measures(type, value)
98
+ found = []
99
+ @sub_measures.each do |sub_measure|
100
+ found << sub_measure if sub_measure[type] and sub_measure[type] == value.id
101
+ end
102
+ found
103
+ end
104
+
105
+ def parse()
106
+ @doc[:logic].each do |key,criteria|
107
+ @population_criteria_by_key[key] = convert(key.to_s, criteria)
108
+ end
109
+ end
110
+
111
+ def convert(key, population_criteria)
112
+
113
+ # @param [String] id
114
+ # @param [Array#Precondition] preconditions
115
+
116
+ preconditions = HQMF::PreconditionConverter.parse_preconditions(population_criteria[:preconditions],@data_criteria_converter)
117
+ hqmf_id = population_criteria[:hqmf_id] || population_criteria[:id]
118
+ id = population_criteria[:id]
119
+ type = population_criteria[:code]
120
+ reference = population_criteria[:reference]
121
+ title = population_criteria[:title]
122
+
123
+ criteria = HQMF::Converter::SimplePopulationCriteria.new(key, hqmf_id, type, preconditions, title)
124
+ # mark the 2.0 simple population criteria as a stratification... this allows us to create the cartesian product for this in the populations
125
+ criteria.stratification_id = population_criteria[:stratification_id]
126
+
127
+ @population_criteria_by_id[id] = criteria
128
+ @population_reference[key] = reference
129
+
130
+ criteria
131
+
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,164 @@
1
+ module HQMF
2
+ # Class for converting an HQMF 1.0 representation to an HQMF 2.0 representation
3
+ class PreconditionConverter
4
+
5
+ def self.parse_preconditions(source,data_criteria_converter)
6
+
7
+ # preconditions = []
8
+ # source.each do |precondition|
9
+ # preconditions << HQMF::PreconditionConverter.parse_precondition(precondition,data_criteria_converter)
10
+ # end
11
+ #
12
+ # preconditions
13
+
14
+ parse_and_merge_preconditions(source,data_criteria_converter)
15
+ end
16
+
17
+ # converts a precondtion to a hqmf model
18
+ def self.parse_precondition(precondition,data_criteria_converter)
19
+
20
+ # grab child preconditions, and parse recursively
21
+ preconditions = parse_and_merge_preconditions(precondition[:preconditions],data_criteria_converter) if precondition[:preconditions] || []
22
+
23
+ preconditions_from_restrictions = HQMF::PreconditionExtractor.extract_preconditions_from_restrictions(precondition[:restrictions], data_criteria_converter)
24
+
25
+ # driv preconditions are preconditions that are the children of an expression
26
+ driv_preconditions = []
27
+ preconditions_from_restrictions.delete_if {|element| driv_preconditions << element if element.is_a? HQMF::Converter::SimpleRestriction and element.operator.type == 'DRIV'}
28
+
29
+ apply_restrictions_to_comparisons(preconditions, preconditions_from_restrictions) unless preconditions_from_restrictions.empty?
30
+
31
+ conjunction_code = convert_logical_conjunction(precondition[:conjunction])
32
+
33
+ if (precondition[:expression])
34
+ # this is for things like COUNT
35
+ type = precondition[:expression][:type]
36
+ operator = HQMF::Converter::SimpleOperator.new(HQMF::Converter::SimpleOperator.find_category(type), type, HQMF::Converter::SimpleOperator.parse_value(precondition[:expression][:value]))
37
+ children = []
38
+ if driv_preconditions and !driv_preconditions.empty?
39
+ children = driv_preconditions.map(&:preconditions).flatten
40
+ end
41
+
42
+ reference = nil
43
+ # take the conjunction code from the parent precondition
44
+
45
+ restriction = HQMF::Converter::SimpleRestriction.new(operator, nil, children)
46
+
47
+ comparison_precondition = HQMF::Converter::SimplePrecondition.new(nil,[restriction],reference,conjunction_code, false)
48
+ comparison_precondition.klass = HQMF::Converter::SimplePrecondition::COMPARISON
49
+ comparison_precondition.subset_comparison = true
50
+ preconditions << comparison_precondition
51
+ end
52
+
53
+ reference = nil
54
+
55
+ negation = precondition[:negation]
56
+
57
+
58
+ if (precondition[:comparison])
59
+ preconditions ||= []
60
+ comparison_precondition = HQMF::PreconditionExtractor.convert_comparison_to_precondition(precondition[:comparison],data_criteria_converter)
61
+ preconditions << comparison_precondition
62
+ end
63
+
64
+
65
+ if (precondition[:subset])
66
+ # this is for things like FIRST on preconditions
67
+ type = precondition[:subset]
68
+ operator = HQMF::Converter::SimpleOperator.new(HQMF::Converter::SimpleOperator.find_category(type), type, nil)
69
+ children = preconditions
70
+
71
+ reference = nil
72
+ # take the conjunction code from the parent precondition
73
+
74
+ restriction = HQMF::Converter::SimpleRestriction.new(operator, nil, children)
75
+
76
+ comparison_precondition = HQMF::Converter::SimplePrecondition.new(nil,[restriction],reference,conjunction_code, false)
77
+ comparison_precondition.klass = HQMF::Converter::SimplePrecondition::COMPARISON
78
+ preconditions = [comparison_precondition]
79
+ end
80
+
81
+
82
+ HQMF::Converter::SimplePrecondition.new(nil,preconditions,reference,conjunction_code, negation)
83
+
84
+ end
85
+
86
+ def self.get_comparison_preconditions(preconditions)
87
+ comparisons = []
88
+ preconditions.each do |precondition|
89
+ if (precondition.comparison? and !precondition.subset_comparison)
90
+ comparisons << precondition
91
+ elsif(precondition.has_preconditions?)
92
+ comparisons.concat(get_comparison_preconditions(precondition.preconditions))
93
+ else
94
+ raise "precondition with no comparison or children... not valid"
95
+ end
96
+ end
97
+ comparisons
98
+ end
99
+
100
+ def self.apply_restrictions_to_comparisons(preconditions, restrictions)
101
+ comparisons = get_comparison_preconditions(preconditions)
102
+ raise "no comparisons to apply restriction to" if comparisons.empty?
103
+ comparisons.each do |comparison|
104
+ comparison.preconditions.concat(restrictions)
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+
111
+ def self.parse_and_merge_preconditions(source,data_criteria_converter)
112
+ return [] unless source and source.size > 0
113
+ preconditions_by_conjunction = {}
114
+ source.each do |precondition|
115
+ parsed = HQMF::PreconditionConverter.parse_precondition(precondition,data_criteria_converter)
116
+ preconditions_by_conjunction[parsed.conjunction_code] ||= []
117
+ preconditions_by_conjunction[parsed.conjunction_code] << parsed
118
+ end
119
+
120
+ merge_precondtion_conjunction_groups(preconditions_by_conjunction)
121
+ end
122
+
123
+ def self.merge_precondtion_conjunction_groups(preconditions_by_conjunction)
124
+ joined = []
125
+ preconditions_by_conjunction.each do |conjunction_code, preconditions|
126
+ sub_conditions = []
127
+ negated_conditions = []
128
+ preconditions.each do |precondition|
129
+ unless (precondition.negation)
130
+ sub_conditions.concat precondition.preconditions if precondition.preconditions
131
+ else
132
+ negated_conditions.concat precondition.preconditions if precondition.preconditions
133
+ end
134
+ end
135
+
136
+ if (!sub_conditions.empty?)
137
+ # if we have negated conditions, add them to a new precondition of the same conjunction that is negated
138
+ if (!negated_conditions.empty?)
139
+ sub_conditions << HQMF::Converter::SimplePrecondition.new(nil,negated_conditions,nil,conjunction_code, true)
140
+ end
141
+ joined << HQMF::Converter::SimplePrecondition.new(nil,sub_conditions,nil,conjunction_code, false)
142
+ elsif (!negated_conditions.empty?)
143
+ joined << HQMF::Converter::SimplePrecondition.new(nil,negated_conditions,nil,conjunction_code, true)
144
+ end
145
+
146
+ end
147
+ joined
148
+ end
149
+
150
+ def self.convert_logical_conjunction(code)
151
+ case code
152
+ when 'OR'
153
+ HQMF::Precondition::AT_LEAST_ONE_TRUE
154
+ when 'AND'
155
+ HQMF::Precondition::ALL_TRUE
156
+ else
157
+ raise "unsupported logical conjunction code conversion: #{code}"
158
+ end
159
+
160
+ end
161
+
162
+
163
+ end
164
+ end