hqmf-parser 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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