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,52 @@
1
+ module HQMF1
2
+
3
+ class Expression
4
+
5
+ include HQMF1::Utilities
6
+
7
+ attr_reader :text, :value, :type
8
+
9
+ def initialize(entry)
10
+ @entry = entry
11
+ @text = @entry.xpath('./*/cda:derivationExpr').text.strip
12
+ type = attr_val('./*/cda:value/@xsi:type')
13
+ case type
14
+ when 'IVL_PQ'
15
+ @value = Range.new(@entry.xpath('./*/cda:value'))
16
+ when 'PQ'
17
+ @value = Value.new(@entry.xpath('./*/cda:value'))
18
+ when 'ANYNonNull'
19
+ @value = HQMF::AnyValue.new
20
+ else
21
+ raise "Unknown expression value type #{type}"
22
+ end
23
+ @type = match_type
24
+ end
25
+
26
+ def match_type
27
+ case @text
28
+ when /^COUNT(.*)$/
29
+ "COUNT"
30
+ when /^MIN(.*)$/
31
+ "MIN"
32
+ when /^MAX(.*)$/
33
+ "MAX"
34
+ when /^DATEDIFF(.*)$/
35
+ "DATEDIFF"
36
+ else
37
+ raise "unknown expression type: #{@text}"
38
+ end
39
+ end
40
+
41
+ def to_json
42
+
43
+ json = build_hash(self, [:text,:type])
44
+ json[:value] = self.value.to_json if self.value
45
+ json
46
+
47
+ end
48
+
49
+ end
50
+
51
+
52
+ end
@@ -0,0 +1,79 @@
1
+ module HQMF1
2
+ # Represents an HQMF population criteria
3
+ class PopulationCriteria
4
+
5
+ include HQMF1::Utilities
6
+
7
+ attr_reader :preconditions, :entry, :doc
8
+ attr_accessor :id, :hqmf_id, :stratification_id
9
+
10
+ # Create a new population criteria from the supplied HQMF entry
11
+ # @param [Nokogiri::XML::Element] the HQMF entry
12
+ def initialize(entry, doc)
13
+ @doc = doc
14
+ @entry = entry
15
+ @id = attr_val('cda:observation/cda:id/@root').upcase
16
+ @preconditions = @entry.xpath('./*/cda:sourceOf[@typeCode="PRCN"]').collect do |entry|
17
+ pc = Precondition.new(entry, nil, @doc)
18
+ if pc.preconditions.length==0 && !pc.comparison && pc.restrictions.length==0
19
+ nil
20
+ else
21
+ pc
22
+ end
23
+ end.compact
24
+ end
25
+
26
+ # Get the code for the population criteria
27
+ # @return [String] the code (e.g. IPP, DEMON, NUMER, DENEX, EXCEP)
28
+ def code
29
+ value = attr_val('cda:observation/cda:value/@code')
30
+ # exclusion population criteria has id of DENOM with actionNegationInd of true
31
+ # special case this to simply handling
32
+ if attr_val('cda:observation/@actionNegationInd')=='true'
33
+ value = HQMF::PopulationCriteria::DENEX
34
+ end
35
+ # replace measure population with NUMER. MSRPOPL is used in continuous variable calculations.
36
+ value = HQMF::PopulationCriteria::NUMER if value == 'MSRPOPL'
37
+ # replace DENEXCEP with EXCEP. DENEXCEP is used by the MAT instead of EXCEP as required by HQMF V2 and QRDA.
38
+ value = HQMF::PopulationCriteria::EXCEP if value == 'DENEXCEP'
39
+ value.upcase
40
+ end
41
+
42
+ # Get the id for the population criteria, used elsewhere in the HQMF document to
43
+ # refer to this criteria
44
+ # @return [String] the id
45
+ def id
46
+ @id
47
+ end
48
+
49
+ def title
50
+ attr_val('cda:observation/cda:value/@displayName')
51
+ end
52
+
53
+ def reference
54
+ reference = attr_val('./cda:observation/cda:sourceOf[@typeCode="PRCN"]/cda:observation[@classCode="OBS"]/cda:id/@root')
55
+ reference = reference.upcase if reference
56
+ reference
57
+ end
58
+
59
+ def to_json
60
+
61
+ json = {}
62
+ self.preconditions.compact.each do |precondition|
63
+ json[:preconditions] ||= []
64
+ json[:preconditions] << precondition.to_json
65
+ end
66
+
67
+ json[:id] = id
68
+ json[:title] = title
69
+ json[:code] = code
70
+ json[:hqmf_id] = hqmf_id if hqmf_id
71
+ json[:stratification_id] = stratification_id if stratification_id
72
+ json[:reference] = reference
73
+
74
+ {self.code => json}
75
+
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,89 @@
1
+ module HQMF1
2
+
3
+ class Precondition
4
+
5
+ include HQMF1::Utilities
6
+
7
+ attr_reader :restrictions, :preconditions, :subset, :expression, :id
8
+
9
+ def initialize(entry, parent, doc)
10
+ @doc = doc
11
+ @entry = entry
12
+ @id = attr_val('./*/cda:id/@root')
13
+ @restrictions = []
14
+
15
+ local_subset = attr_val('./cda:subsetCode/@code')
16
+ if local_subset
17
+ @subset = local_subset
18
+ end
19
+ #@subset = attr_val('./cda:subsetCode/@code')
20
+
21
+ local_restrictions = @entry.xpath('./*/cda:sourceOf[@typeCode!="PRCN" and @typeCode!="COMP"]').collect do |entry|
22
+ Restriction.new(entry, self, @doc)
23
+ end
24
+ @restrictions.concat(local_restrictions)
25
+
26
+ @expression = Expression.new(@entry) if @entry.at_xpath('./*/cda:derivationExpr')
27
+
28
+ @preconditions = @entry.xpath('./*/cda:sourceOf[@typeCode="PRCN"]').collect do |entry|
29
+ Precondition.new(entry, self, @doc)
30
+ end
31
+ end
32
+
33
+ # Get the conjunction code, e.g. AND, OR
34
+ # @return [String] conjunction code
35
+ def conjunction
36
+ attr_val('./cda:conjunctionCode/@code')
37
+ end
38
+
39
+ # Return whether the precondition is negated (true) or not (false)
40
+ def negation
41
+ if @entry.at_xpath('./cda:act[@actionNegationInd="true"]')
42
+ is_negation_rationale = (comparison.restrictions.map {|restriction| restriction.type }).include? 'RSON' if comparison
43
+ if is_negation_rationale
44
+ false
45
+ else
46
+ true
47
+ end
48
+ else
49
+ false
50
+ end
51
+ end
52
+
53
+ def comparison
54
+ comparison_def = @entry.at_xpath('./*/cda:sourceOf[@typeCode="COMP"]')
55
+ if comparison_def
56
+ data_criteria_id = attr_val('./*/cda:id/@root')
57
+ @comparison = Comparison.new(data_criteria_id, comparison_def, self, @doc)
58
+ end
59
+ end
60
+
61
+ def first_comparison
62
+ if comparison
63
+ return comparison
64
+ elsif @preconditions
65
+ @preconditions.each do |precondition|
66
+ first = precondition.first_comparison
67
+ if first
68
+ return first
69
+ end
70
+ end
71
+ end
72
+ return nil
73
+ end
74
+
75
+ def to_json
76
+
77
+ json = build_hash(self, [:id,:conjunction,:negation,:subset])
78
+ json[:comparison] = self.comparison.to_json if self.comparison
79
+ json[:expression] = self.expression.to_json if self.expression
80
+ json[:preconditions] = json_array(self.preconditions)
81
+ json[:restrictions] = json_array(self.restrictions)
82
+ json
83
+
84
+ end
85
+
86
+ end
87
+
88
+
89
+ end
@@ -0,0 +1,65 @@
1
+ module HQMF1
2
+ # Represents a bound within a HQMF pauseQuantity, has a value, a unit and an
3
+ # inclusive/exclusive indicator
4
+ class Value
5
+ include HQMF1::Utilities
6
+
7
+ def initialize(entry)
8
+ @entry = entry
9
+ end
10
+
11
+ def value
12
+ attr_val('./@value')
13
+ end
14
+
15
+ def unit
16
+ attr_val('./@unit')
17
+ end
18
+
19
+ def inclusive?
20
+ case attr_val('./@inclusive')
21
+ when 'true'
22
+ true
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def to_json
29
+ build_hash(self, [:value,:unit,:inclusive?])
30
+ end
31
+ end
32
+
33
+ # Represents a HQMF pauseQuantity which can have low and high bounds
34
+ class Range
35
+ include HQMF1::Utilities
36
+ attr_reader :low, :high
37
+
38
+ def initialize(entry)
39
+ @entry = entry
40
+ if @entry
41
+ @low = optional_value('./cda:low')
42
+ @high = optional_value('./cda:high')
43
+ end
44
+ end
45
+
46
+ def to_json
47
+ json = {}
48
+ json[:low] = self.low.to_json if self.low
49
+ json[:high] = self.high.to_json if self.high
50
+ json
51
+ end
52
+
53
+ private
54
+
55
+ def optional_value(xpath)
56
+ value_def = @entry.at_xpath(xpath)
57
+ if value_def
58
+ Value.new(value_def)
59
+ else
60
+ nil
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,157 @@
1
+ module HQMF1
2
+ # Represents a restriction on the allowable values of a data item
3
+ class Restriction
4
+
5
+ include HQMF1::Utilities
6
+
7
+ attr_reader :range, :comparison, :restrictions, :subset, :preconditions
8
+ attr_accessor :from_parent
9
+
10
+ def initialize(entry, parent, doc)
11
+ @doc = doc
12
+ @entry = entry
13
+ @restrictions = []
14
+
15
+ range_def = @entry.at_xpath('./cda:pauseQuantity')
16
+ if range_def
17
+ @range = Range.new(range_def)
18
+ end
19
+
20
+ local_restrictions = @entry.xpath('./*/cda:sourceOf[@typeCode!="PRCN" and @typeCode!="COMP"]').collect do |entry|
21
+ Restriction.new(entry, self, @doc)
22
+ end
23
+
24
+ @restrictions.concat(local_restrictions)
25
+
26
+ local_subset = attr_val('./cda:subsetCode/@code')
27
+ if local_subset
28
+ @subset = local_subset
29
+ end
30
+
31
+ #@subset = attr_val('./cda:subsetCode/@code')
32
+
33
+ comparison_def = @entry.at_xpath('./*/cda:sourceOf[@typeCode="COMP"]')
34
+ if comparison_def
35
+ data_criteria_id = attr_val('./*/cda:id/@root')
36
+ data_criteria_id = comparison_def.at_xpath('./*/cda:id/@root').value if (data_criteria_id.nil? and comparison_def.at_xpath('./*/cda:id/@root'))
37
+ @comparison = Comparison.new(data_criteria_id, comparison_def, self, @doc)
38
+ end
39
+
40
+ @preconditions = @entry.xpath('./*/cda:sourceOf[@typeCode="PRCN"]').collect do |entry|
41
+ # create a dummy parent with a single restriction copied from self minus the
42
+ # nested preconditions to avoid an infinite loop
43
+ prior_comparison = nil
44
+ if parent.class==HQMF1::Precondition
45
+ prior_comparison = parent.first_comparison
46
+ else
47
+ prior_comparison = @comparsion
48
+ end
49
+ current_restriction = OpenStruct.new(
50
+ 'range' => @range,
51
+ 'comparison' => prior_comparison,
52
+ 'restrictions' => [],
53
+ 'preconditions' => [],
54
+ 'subset' => @subset,
55
+ 'type' => type,
56
+ 'target_id' => target_id,
57
+ 'field' => field,
58
+ 'field_code' => field_code,
59
+ 'field_time' => field_time,
60
+ 'value' => value)
61
+ all_restrictions = []
62
+ all_restrictions.concat @restrictions
63
+ all_restrictions << current_restriction
64
+ parent = OpenStruct.new(
65
+ 'restrictions' => all_restrictions,
66
+ 'subset' => @subset
67
+ )
68
+ p = Precondition.new(entry, parent, @doc)
69
+
70
+ end
71
+
72
+ end
73
+
74
+ # The type of restriction, e.g. SBS, SBE etc
75
+ def type
76
+ attr_val('./@typeCode')
77
+ end
78
+
79
+ # is this type negated? true or false
80
+ def negation
81
+ attr_val('./@inversionInd') == "true"
82
+ end
83
+
84
+ # The id of the data criteria or measurement property that the value
85
+ # will be compared against
86
+ def target_id
87
+ attr_val('./*/cda:id/@root')
88
+ end
89
+
90
+ def field
91
+ attr_val('./cda:observation/cda:code/@displayName')
92
+ end
93
+
94
+ def field_code
95
+ attr_val('./cda:observation/cda:code/@code') || attr_val('./cda:encounter/cda:participant/cda:roleParticipant/@classCode')
96
+ end
97
+
98
+ def field_time
99
+ effectiveTime = @entry.at_xpath('./cda:observation/cda:effectiveTime') || @entry.at_xpath('./cda:encounter/cda:participant/cda:roleParticipant/cda:effectiveTime')
100
+
101
+ time = nil
102
+ if effectiveTime
103
+ time = :start if effectiveTime.at_xpath('./cda:low')
104
+ time = :end if effectiveTime.at_xpath('./cda:high')
105
+ end
106
+ time
107
+ end
108
+
109
+
110
+ def value
111
+ value = nil
112
+ type = attr_val('./cda:observation/cda:value/@xsi:type') || 'CD'
113
+ case type
114
+ when 'IVL_PQ'
115
+ value = HQMF1::Range.new(@entry.xpath('./cda:observation/cda:value'))
116
+ when 'PQ'
117
+ value = HQMF1::Value.new(@entry.xpath('./cda:observation/cda:value'))
118
+ when 'CD'
119
+ if field && field.downcase == 'status'
120
+ code = attr_val('./cda:observation/cda:value/@displayName').downcase
121
+ value = HQMF::Coded.for_single_code('status',code,code)
122
+ elsif attr_val('./cda:observation/cda:value/@code')
123
+ oid = attr_val('./cda:observation/cda:value/@code')
124
+ title = attr_val('./cda:observation/cda:value/@displayName')
125
+ value = HQMF::Coded.for_code_list(oid,title)
126
+ elsif attr_val('./cda:encounter/cda:participant/cda:roleParticipant/cda:code/@code')
127
+ oid = attr_val('./cda:encounter/cda:participant/cda:roleParticipant/cda:code/@code')
128
+ title = attr_val('./cda:encounter/cda:participant/cda:roleParticipant/cda:code/@displayName')
129
+ value = HQMF::Coded.for_code_list(oid,title)
130
+ end
131
+ when 'ANYNonNull'
132
+ value = HQMF::AnyValue.new
133
+ else
134
+ raise "Unknown restriction value type #{type}"
135
+ end if type
136
+ value
137
+ end
138
+
139
+ def to_json
140
+ return nil if from_parent
141
+ json = build_hash(self, [:subset,:type,:target_id,:field,:field_code,:from_parent, :negation, :field_time])
142
+ json[:range] = range.to_json if range
143
+ if value
144
+ if value.is_a? String
145
+ json[:value] = value
146
+ else
147
+ json[:value] = value.to_json
148
+ end
149
+ end
150
+ json[:comparison] = comparison.to_json if comparison
151
+ json[:restrictions] = json_array(self.restrictions)
152
+ json[:preconditions] = json_array(self.preconditions)
153
+ json
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,41 @@
1
+ module HQMF1
2
+ module Utilities
3
+
4
+ include HQMF::Conversion::Utilities
5
+
6
+ # Utility function to handle optional attributes
7
+ # @param xpath an XPath that identifies an XML attribute
8
+ # @return the value of the attribute or nil if the attribute is missing
9
+ def attr_val(xpath)
10
+ attr = @entry.at_xpath(xpath)
11
+ if attr
12
+ attr.value
13
+ else
14
+ nil
15
+ end
16
+ end
17
+
18
+ def clean_json(json)
19
+ json.reject!{|k,v| v.nil? || (v.respond_to?(:empty?) && v.empty?)}
20
+ end
21
+
22
+ def clean_json_recursive(json)
23
+ json.each do |k,v|
24
+ if v.is_a? Hash
25
+ clean_json_recursive(v)
26
+ clean_json(v)
27
+ elsif v.is_a? Array
28
+ v.each do |e|
29
+ if e.is_a? Hash
30
+ clean_json_recursive(e)
31
+ clean_json(e)
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ clean_json(json)
38
+ end
39
+
40
+ end
41
+ end