cqm-parsers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +29 -0
  3. data/README.md +21 -0
  4. data/Rakefile +19 -0
  5. data/lib/ext/code.rb +10 -0
  6. data/lib/ext/data_element.rb +24 -0
  7. data/lib/hqmf-model/attribute.rb +63 -0
  8. data/lib/hqmf-model/data_criteria.rb +467 -0
  9. data/lib/hqmf-model/document.rb +253 -0
  10. data/lib/hqmf-model/population_criteria.rb +102 -0
  11. data/lib/hqmf-model/precondition.rb +94 -0
  12. data/lib/hqmf-model/types.rb +457 -0
  13. data/lib/hqmf-model/utilities.rb +52 -0
  14. data/lib/hqmf-parser.rb +116 -0
  15. data/lib/hqmf-parser/1.0/attribute.rb +121 -0
  16. data/lib/hqmf-parser/1.0/comparison.rb +34 -0
  17. data/lib/hqmf-parser/1.0/data_criteria.rb +92 -0
  18. data/lib/hqmf-parser/1.0/document.rb +195 -0
  19. data/lib/hqmf-parser/1.0/expression.rb +60 -0
  20. data/lib/hqmf-parser/1.0/observation.rb +61 -0
  21. data/lib/hqmf-parser/1.0/population_criteria.rb +75 -0
  22. data/lib/hqmf-parser/1.0/precondition.rb +90 -0
  23. data/lib/hqmf-parser/1.0/range.rb +76 -0
  24. data/lib/hqmf-parser/1.0/restriction.rb +162 -0
  25. data/lib/hqmf-parser/1.0/utilities.rb +55 -0
  26. data/lib/hqmf-parser/2.0/data_criteria.rb +372 -0
  27. data/lib/hqmf-parser/2.0/data_criteria_helpers/dc_base_extract.rb +80 -0
  28. data/lib/hqmf-parser/2.0/data_criteria_helpers/dc_definition_from_template_or_type_extract.rb +201 -0
  29. data/lib/hqmf-parser/2.0/data_criteria_helpers/dc_post_processing.rb +85 -0
  30. data/lib/hqmf-parser/2.0/data_criteria_helpers/dc_specific_occurrences_and_source_data_criteria_extract.rb +117 -0
  31. data/lib/hqmf-parser/2.0/document.rb +304 -0
  32. data/lib/hqmf-parser/2.0/document_helpers/doc_population_helper.rb +173 -0
  33. data/lib/hqmf-parser/2.0/document_helpers/doc_utilities.rb +131 -0
  34. data/lib/hqmf-parser/2.0/field_value_helper.rb +251 -0
  35. data/lib/hqmf-parser/2.0/population_criteria.rb +134 -0
  36. data/lib/hqmf-parser/2.0/precondition.rb +73 -0
  37. data/lib/hqmf-parser/2.0/source_data_criteria_helper.rb +112 -0
  38. data/lib/hqmf-parser/2.0/types.rb +448 -0
  39. data/lib/hqmf-parser/2.0/utilities.rb +45 -0
  40. data/lib/hqmf-parser/2.0/value_set_helper.rb +104 -0
  41. data/lib/hqmf-parser/converter/pass1/data_criteria_converter.rb +257 -0
  42. data/lib/hqmf-parser/converter/pass1/document_converter.rb +133 -0
  43. data/lib/hqmf-parser/converter/pass1/population_criteria_converter.rb +185 -0
  44. data/lib/hqmf-parser/converter/pass1/precondition_converter.rb +173 -0
  45. data/lib/hqmf-parser/converter/pass1/precondition_extractor.rb +201 -0
  46. data/lib/hqmf-parser/converter/pass1/simple_data_criteria.rb +26 -0
  47. data/lib/hqmf-parser/converter/pass1/simple_operator.rb +89 -0
  48. data/lib/hqmf-parser/converter/pass1/simple_population_criteria.rb +10 -0
  49. data/lib/hqmf-parser/converter/pass1/simple_precondition.rb +51 -0
  50. data/lib/hqmf-parser/converter/pass1/simple_restriction.rb +64 -0
  51. data/lib/hqmf-parser/converter/pass2/comparison_converter.rb +112 -0
  52. data/lib/hqmf-parser/converter/pass2/operator_converter.rb +102 -0
  53. data/lib/hqmf-parser/cql/data_criteria.rb +57 -0
  54. data/lib/hqmf-parser/cql/data_criteria_helpers/dc_definition_from_template_or_type_extract.rb +79 -0
  55. data/lib/hqmf-parser/cql/data_criteria_helpers/dc_post_processing.rb +43 -0
  56. data/lib/hqmf-parser/cql/document.rb +78 -0
  57. data/lib/hqmf-parser/cql/document_helpers/doc_population_helper.rb +124 -0
  58. data/lib/hqmf-parser/cql/value_set_helper.rb +103 -0
  59. data/lib/hqmf-parser/parser.rb +100 -0
  60. data/lib/qrda-export/catI-r5/qrda1_r5.rb +125 -0
  61. data/lib/qrda-export/helper/cat_1_view_helper.rb +142 -0
  62. data/lib/qrda-export/helper/code_system_helper.rb +77 -0
  63. data/lib/qrda-export/helper/date_helper.rb +81 -0
  64. data/lib/qrda-import/base-importers/demographics_importer.rb +47 -0
  65. data/lib/qrda-import/base-importers/medication_importer.rb +22 -0
  66. data/lib/qrda-import/base-importers/section_importer.rb +196 -0
  67. data/lib/qrda-import/cda_identifier.rb +19 -0
  68. data/lib/qrda-import/data-element-importers/adverse_event_importer.rb +23 -0
  69. data/lib/qrda-import/data-element-importers/allergy_intolerance_importer.rb +21 -0
  70. data/lib/qrda-import/data-element-importers/assessment_performed_importer.rb +23 -0
  71. data/lib/qrda-import/data-element-importers/communication_from_patient_to_provider_importer.rb +18 -0
  72. data/lib/qrda-import/data-element-importers/communication_from_provider_to_patient_importer.rb +18 -0
  73. data/lib/qrda-import/data-element-importers/communication_from_provider_to_provider_importer.rb +20 -0
  74. data/lib/qrda-import/data-element-importers/device_applied_importer.rb +23 -0
  75. data/lib/qrda-import/data-element-importers/device_order_importer.rb +18 -0
  76. data/lib/qrda-import/data-element-importers/diagnosis_importer.rb +23 -0
  77. data/lib/qrda-import/data-element-importers/diagnostic_study_order_importer.rb +20 -0
  78. data/lib/qrda-import/data-element-importers/diagnostic_study_performed_importer.rb +30 -0
  79. data/lib/qrda-import/data-element-importers/encounter_order_importer.rb +20 -0
  80. data/lib/qrda-import/data-element-importers/encounter_performed_importer.rb +41 -0
  81. data/lib/qrda-import/data-element-importers/immunization_administered_importer.rb +18 -0
  82. data/lib/qrda-import/data-element-importers/intervention_order_importer.rb +18 -0
  83. data/lib/qrda-import/data-element-importers/intervention_performed_importer.rb +22 -0
  84. data/lib/qrda-import/data-element-importers/laboratory_test_order_importer.rb +20 -0
  85. data/lib/qrda-import/data-element-importers/laboratory_test_performed_importer.rb +28 -0
  86. data/lib/qrda-import/data-element-importers/medication_active_importer.rb +17 -0
  87. data/lib/qrda-import/data-element-importers/medication_administered_importer.rb +17 -0
  88. data/lib/qrda-import/data-element-importers/medication_discharge_importer.rb +19 -0
  89. data/lib/qrda-import/data-element-importers/medication_dispensed_importer.rb +19 -0
  90. data/lib/qrda-import/data-element-importers/medication_order_importer.rb +16 -0
  91. data/lib/qrda-import/data-element-importers/patient_characteristic_expired.rb +21 -0
  92. data/lib/qrda-import/data-element-importers/physical_exam_performed_importer.rb +26 -0
  93. data/lib/qrda-import/data-element-importers/procedure_order_importer.rb +26 -0
  94. data/lib/qrda-import/data-element-importers/procedure_performed_importer.rb +34 -0
  95. data/lib/qrda-import/data-element-importers/substance_administered_importer.rb +16 -0
  96. data/lib/qrda-import/entry_finder.rb +20 -0
  97. data/lib/qrda-import/entry_package.rb +16 -0
  98. data/lib/qrda-import/narrative_reference_handler.rb +33 -0
  99. data/lib/qrda-import/patient_importer.rb +105 -0
  100. data/lib/util/code_system_helper.rb +76 -0
  101. data/lib/util/counter.rb +20 -0
  102. data/lib/util/hqmf_template_helper.rb +39 -0
  103. metadata +340 -0
@@ -0,0 +1,134 @@
1
+ module HQMF2
2
+ # Represents an HQMF population criteria, also supports all the same methods as
3
+ # HQMF2::Precondition
4
+ class PopulationCriteria
5
+ include HQMF2::Utilities
6
+
7
+ attr_reader :preconditions, :id, :hqmf_id, :title, :aggregator, :comments
8
+ # need to do this to allow for setting the type to OBSERV for
9
+ attr_accessor :type
10
+ # Create a new population criteria from the supplied HQMF entry
11
+ # @param [Nokogiri::XML::Element] the HQMF entry
12
+ def initialize(entry, doc, id_generator)
13
+ @id_generator = id_generator
14
+ @doc = doc
15
+ @entry = entry
16
+ setup_derived_entry_elements(id_generator)
17
+ # modify type to meet current expected population names
18
+ @type = 'IPP' if @type == 'IPOP' || @type == 'IPPOP'
19
+ @comments = nil if comments.empty?
20
+ # MEAN is handled in current code. Changed since it should have the same effect
21
+ @aggregator = 'MEAN' if @aggregator == 'AVERAGE'
22
+ @hqmf_id = @type unless @hqmf_id # The id extension is not required, if it's not provided use the code
23
+ handle_type(id_generator)
24
+ end
25
+
26
+ # Handles how the code should deal with the type definition (aggregate vs non-aggregate)
27
+ def handle_type(id_generator)
28
+ if @type != 'AGGREGATE'
29
+ # Generate the precondition for this population
30
+ if @preconditions.length > 1 ||
31
+ (@preconditions.length == 1 && @preconditions[0].conjunction != conjunction_code)
32
+ @preconditions = [Precondition.new(id_generator.next_id, conjunction_code, @preconditions)]
33
+ end
34
+ else
35
+ # Extract the data criteria this population references
36
+ dc = handle_observation_criteria
37
+ @preconditions = [Precondition.new(id_generator.next_id, nil, nil, false, HQMF2::Reference.new(dc.id))] if dc
38
+ end
39
+ end
40
+
41
+ # Handles extracting elements from the entry
42
+ def setup_derived_entry_elements(id_generator)
43
+ @hqmf_id = attr_val('./*/cda:id/@root') || attr_val('./*/cda:typeId/@extension')
44
+ @title = attr_val('./*/cda:code/cda:displayName/@value').try(:titleize)
45
+ @type = attr_val('./*/cda:code/@code')
46
+ @comments = @entry.xpath('./*/cda:text/cda:xml/cda:qdmUserComments/cda:item/text()', HQMF2::Document::NAMESPACES)
47
+ .map(&:content)
48
+ handle_preconditions(id_generator)
49
+ obs_test = attr_val('./cda:measureObservationDefinition/@classCode')
50
+ # If there are no measure observations, or there is a title, then there are no aggregations to extract
51
+ return unless !@title && obs_test.to_s == 'OBS'
52
+ @title = attr_val('../cda:code/cda:displayName/@value')
53
+ @aggregator = attr_val('./cda:measureObservationDefinition/cda:methodCode/cda:item/@code')
54
+ end
55
+
56
+ # specifically handles extracting the preconditions for the population criteria
57
+ def handle_preconditions(id_generator)
58
+ # Nest multiple preconditions under a single root precondition
59
+ @preconditions = @entry.xpath('./*/cda:precondition[not(@nullFlavor)]', HQMF2::Document::NAMESPACES)
60
+ .collect do |pre|
61
+ precondition = Precondition.parse(pre, @doc, id_generator)
62
+ precondition.reference.nil? && precondition.preconditions.empty? ? nil : precondition
63
+ end
64
+ # Remove uneeded nils from the array
65
+ @preconditions.compact!
66
+ end
67
+
68
+ # extracts out any measure observation definitons, creating from them the proper criteria to generate a precondition
69
+ def handle_observation_criteria
70
+ exp = @entry.at_xpath('./cda:measureObservationDefinition/cda:value/cda:expression/@value',
71
+ HQMF2::Document::NAMESPACES)
72
+ # Measure Observations criteria rely on computed expressions. If it doesn't have one,
73
+ # then it is likely formatted improperly.
74
+ fail 'Measure Observations criteria is missing computed expression(s) ' if exp.nil?
75
+ parts = exp.to_s.split('-')
76
+ dc = parse_parts_to_dc(parts)
77
+ @doc.add_data_criteria(dc)
78
+ # Update reference_ids with any newly referenced data criteria
79
+ dc.children_criteria.each { |cc| @doc.add_reference_id(cc) } unless dc&.children_criteria.nil?
80
+ dc
81
+ end
82
+
83
+ # generates the value given in an expression based on the number of criteria it references.
84
+ def parse_parts_to_dc(parts)
85
+ case parts.length
86
+ when 1
87
+ # If there is only one part, it is a reference to an existing data criteria's value
88
+ @doc.find_criteria_by_lvn(parts.first.strip.split('.')[0])
89
+ when 2
90
+ # If there are two parts, there is a computation performed, specifically time difference, on the two criteria
91
+ children = parts.collect { |p| @doc.find_criteria_by_lvn(p.strip.split('.')[0]).id }
92
+ id = "GROUP_TIMEDIFF_#{@id_generator.next_id}"
93
+ HQMF2::DataCriteriaWrapper.new(id: id,
94
+ title: id,
95
+ subset_operators: [HQMF::SubsetOperator.new('DATETIMEDIFF', nil)],
96
+ children_criteria: children,
97
+ derivation_operator: HQMF::DataCriteria::XPRODUCT,
98
+ type: 'derived',
99
+ definition: 'derived',
100
+ negation: false,
101
+ source_data_criteria: id
102
+ )
103
+ else
104
+ # If there are neither one or 2 parts, the code should fail
105
+ fail "No defined extraction method to handle #{parts.length} parts"
106
+ end
107
+ end
108
+
109
+ def create_human_readable_id(id)
110
+ @id = id
111
+ end
112
+
113
+ # Get the conjunction code, ALL_TRUE or AT_LEAST_ONE_TRUE
114
+ # @return [String] conjunction code
115
+ def conjunction_code
116
+ case @type
117
+ when HQMF::PopulationCriteria::IPP, HQMF::PopulationCriteria::DENOM, HQMF::PopulationCriteria::NUMER,
118
+ HQMF::PopulationCriteria::MSRPOPL, HQMF::PopulationCriteria::STRAT
119
+ HQMF::Precondition::ALL_TRUE
120
+ when HQMF::PopulationCriteria::DENEXCEP, HQMF::PopulationCriteria::DENEX, HQMF::PopulationCriteria::MSRPOPLEX,
121
+ HQMF::PopulationCriteria::NUMEX
122
+ HQMF::Precondition::AT_LEAST_ONE_TRUE
123
+ else
124
+ fail "Unknown population type [#{@type}]"
125
+ end
126
+ end
127
+
128
+ # Generates this classes hqmf-model equivalent
129
+ def to_model
130
+ mps = preconditions.collect(&:to_model)
131
+ HQMF::PopulationCriteria.new(id, hqmf_id, type, mps, title, aggregator, comments)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,73 @@
1
+ module HQMF2
2
+ # Represents the logic that defines grouping of criteria and actions done on it.
3
+ class Precondition
4
+ include HQMF2::Utilities
5
+
6
+ attr_reader :preconditions, :reference, :conjunction, :id
7
+
8
+ def self.parse(entry, doc, id_generator)
9
+ doc = doc
10
+ entry = entry
11
+ aggregation = entry.at_xpath('./cda:allTrue | ./cda:atLeastOneTrue | ./cda:allFalse | ./cda:atLeastOneFalse',
12
+ HQMF2::Document::NAMESPACES)
13
+
14
+ # Sets the reference criteria for the precondition (if it exists)
15
+ reference_def = entry.at_xpath('./*/cda:id', HQMF2::Document::NAMESPACES)
16
+ reference_def ||= entry.at_xpath('./cda:join/cda:templateId/cda:item', HQMF2::Document::NAMESPACES)
17
+ reference = Reference.new(reference_def) if reference_def
18
+
19
+ # Unless there is an aggregator, no further actions are necessary.
20
+ return new(id_generator.next_id, nil, [], false, reference) unless aggregation
21
+
22
+ precondition_entries = entry.xpath('./*/cda:precondition', HQMF2::Document::NAMESPACES)
23
+ preconditions = precondition_entries.collect do |precondition|
24
+ precondition = Precondition.parse(precondition, doc, id_generator)
25
+ # There are cases where a precondition may contain no references or preconditions, and should be ignored.
26
+ precondition.reference.nil? && precondition.preconditions.empty? ? nil : precondition
27
+ end
28
+ preconditions.compact!
29
+ handle_aggregation(id_generator, reference, preconditions, aggregation)
30
+ end
31
+
32
+ # "False" aggregators exist, and require special handling, so this manages that and returns the
33
+ # proper precondition.
34
+ def self.handle_aggregation(id_generator, reference, preconditions, aggregation, conjunction = nil)
35
+ negation = false
36
+ conjunction = aggregation.name
37
+ case conjunction
38
+ # DeMorgan's law is used to handle negated caes: e.g. to find if all are false, negate the "at least one true"
39
+ # check.
40
+ when 'allFalse'
41
+ negation = true
42
+ conjunction = 'atLeastOneTrue'
43
+ when 'atLeastOneFalse'
44
+ negation = true
45
+ conjunction = 'allTrue'
46
+ end
47
+ # Return the proper precondition given if a negation exists
48
+ if negation
49
+ # Wrap the negation in a seperate precondition which this will reference
50
+ precondition_wrapper = new(id_generator.next_id, conjunction, preconditions, true, reference)
51
+ new(id_generator.next_id, conjunction, [precondition_wrapper])
52
+ else
53
+ new(id_generator.next_id, conjunction, preconditions, false, reference)
54
+ end
55
+ end
56
+
57
+ def initialize(id, conjunction, preconditions = [], negation = false, reference = nil)
58
+ @preconditions = preconditions || []
59
+ @conjunction = conjunction
60
+ @reference = reference
61
+ @negation = negation
62
+ @id = id
63
+ end
64
+
65
+ # Generates this classes hqmf-model equivalent
66
+ def to_model
67
+ pcs = @preconditions.collect(&:to_model)
68
+ mr = @reference ? @reference.to_model : nil
69
+ cc = @conjunction
70
+ HQMF::Precondition.new(@id, pcs, mr, cc, @negation)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,112 @@
1
+ module HQMF2
2
+ # Generates the Source Data Criteria from the entries in the HQMF
3
+ class SourceDataCriteriaHelper
4
+ # Generates an identifier based on the leftover elements included in the source data criteria.
5
+ def self.identifier(criteria)
6
+ sha256 = ''
7
+ sha256 << "#{criteria.code_list_id}:"
8
+ sha256 << "#{criteria.definition}:"
9
+ sha256 << "#{criteria.status}:"
10
+ sha256 << "#{criteria.specific_occurrence}:"
11
+ sha256 << "#{criteria.specific_occurrence_const}:"
12
+ sha256 << "#{criteria.variable}:"
13
+ sha256 << (criteria.children_criteria.nil? ? '<nil>:' : "#{criteria.children_criteria.sort.join(',')}:")
14
+
15
+ Digest::SHA256.hexdigest sha256
16
+ end
17
+
18
+ # Rejects any derived elements as they should never be used as source.
19
+ def self.should_reject?(dc)
20
+ dc.definition == 'derived'
21
+ end
22
+
23
+ # Removes unnecessary elements from a data criteria to create a source data criteria
24
+ def self.strip_non_sc_elements(dc)
25
+ if [HQMF::DataCriteria::SATISFIES_ANY, HQMF::DataCriteria::SATISFIES_ALL].include? dc.definition
26
+ dc.instance_variable_set(:@definition, 'derived')
27
+ end
28
+ dc.instance_variable_set(:@source_data_criteria, dc.id)
29
+ dc.instance_variable_set(:@field_values, {})
30
+ dc.instance_variable_set(:@temporal_references, [])
31
+ dc.instance_variable_set(:@subset_operators, [])
32
+ dc.instance_variable_set(:@value, nil)
33
+ dc.instance_variable_set(:@negation, false)
34
+ dc.instance_variable_set(:@negation_code_list_id, nil)
35
+ dc
36
+ end
37
+
38
+ # determins if a data criteria has any non-SDC fields set (i.e., those fields need to be stripped)
39
+ def self.already_stripped?(dc)
40
+ dc.field_values.blank? && dc.temporal_references.blank? && dc.subset_operators.blank? && dc.value.blank? && dc.negation.blank? && dc.negation_code_list_id.blank?
41
+ end
42
+
43
+ # Creates a data criteria based on an entry xml, removes any unnecessary elements (for the source),
44
+ # and adds a data criteria reference if none exist
45
+ def self.as_source_data_criteria(entry, data_criteria_references = {}, occurrences_map = {})
46
+ dc = DataCriteria.new(entry, data_criteria_references, occurrences_map)
47
+ dc.original_id = dc.id
48
+ unless dc.definition == 'derived' # && dc.temporal_references.blank? && dc.subset_operators.blank? && dc.value.blank? && dc.field_values.blank?
49
+ # add "_source" to the id to differentiate from the non-source
50
+ dc.id = "#{dc.id}_source"
51
+ end
52
+ dc = SourceDataCriteriaHelper.strip_non_sc_elements(dc)
53
+ # add it as a reference
54
+ if dc && (data_criteria_references[dc.id].nil? || data_criteria_references[dc.id].code_list_id.nil?)
55
+ data_criteria_references[dc.original_id] = dc
56
+ end
57
+
58
+ dc
59
+ end
60
+
61
+ # Check if there is an existing entry in the source data criteria list that matches the candidate passed in
62
+ # this is used to prevent adding duplicate source data criteria entries when one already exists
63
+ def self.find_existing_source_data_criteria(list, candidate)
64
+ list.each do |sdc|
65
+ # check if we have an exact match on an existing SDC
66
+ return sdc if SourceDataCriteriaHelper.identifier(sdc) == SourceDataCriteriaHelper.identifier(candidate)
67
+ # we have another existing copy of the specific occurrence (identified via the constant and occurrence lettering), use that rather than duplicating... there will not be an
68
+ # exact match for variables since a new child will have been generated
69
+ return sdc if !sdc.specific_occurrence_const.nil? && sdc.specific_occurrence_const == candidate.specific_occurrence_const && sdc.specific_occurrence == candidate.specific_occurrence
70
+ end
71
+ nil
72
+ end
73
+
74
+ # Given a list of criteria obtained from the XML, generate most of the source data criteria (since no explicit
75
+ # sources are given). After generating the source data criteria, filter the list to not include repeated,
76
+ # unnecessary sources, but maintain and return map of those that have been removed to those that they were replaced
77
+ # with.
78
+ def self.get_source_data_criteria_list(full_criteria_list, data_criteria_references = {}, occurrences_map = {})
79
+ # currently, this will erase the sources if the ids are the same, but will not correct references later on
80
+ source_data_criteria = full_criteria_list.map do |entry|
81
+ SourceDataCriteriaHelper.as_source_data_criteria(entry, data_criteria_references, occurrences_map)
82
+ end
83
+
84
+ collapsed_source_data_criteria_map = {}
85
+ uniq_source_data_criteria = {}
86
+ source_data_criteria.each do |sdc|
87
+ identifier = SourceDataCriteriaHelper.identifier(sdc)
88
+ if uniq_source_data_criteria.key? identifier
89
+ collapsed_source_data_criteria_map[sdc.original_id] = uniq_source_data_criteria[identifier].id
90
+ else
91
+ uniq_source_data_criteria[identifier] = sdc
92
+ end
93
+ end
94
+ unique = uniq_source_data_criteria.values.reject { |dc| SourceDataCriteriaHelper.should_reject?(dc) }
95
+
96
+ # we need an empty data criteria in source that acts as the target for the specific occurrence
97
+ # the data criteria that we are duplicating will eventually get turned into a specific occurrence
98
+ occurrences = unique.select {|dc| occurrences_map[dc.id] && dc.definition != 'derived' }
99
+ occurrences.each do |occurrence|
100
+ # do not create a nonspecific SDC for variables
101
+ unless occurrence.variable
102
+ dc = SourceDataCriteriaHelper.as_source_data_criteria(occurrence.entry)
103
+ dc.id = "#{dc.id}_nonSpecific"
104
+ dc.instance_variable_set(:@source_data_criteria, dc.id)
105
+ unique << dc unless SourceDataCriteriaHelper.find_existing_source_data_criteria(unique, dc)
106
+ end
107
+ end
108
+
109
+ [unique, collapsed_source_data_criteria_map]
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,448 @@
1
+ module HQMF2
2
+ # Used to represent 'any value' in criteria that require a value be present but
3
+ # don't specify any restrictions on that value
4
+ class AnyValue
5
+ attr_reader :type
6
+
7
+ def initialize(type = 'ANYNonNull')
8
+ @type = type
9
+ end
10
+
11
+ # Generates this classes hqmf-model equivalent
12
+ def to_model
13
+ HQMF::AnyValue.new(@type)
14
+ end
15
+ end
16
+
17
+ # Represents a bound within a HQMF pauseQuantity, has a value, a unit and an
18
+ # inclusive/exclusive indicator
19
+ class Value
20
+ include HQMF2::Utilities
21
+
22
+ attr_reader :type, :unit, :value
23
+
24
+ def initialize(entry, default_type = 'PQ', force_inclusive = false, _parent = nil)
25
+ @entry = entry
26
+ @type = attr_val('./@xsi:type') || default_type
27
+ @unit = attr_val('./@unit')
28
+ @value = attr_val('./@value')
29
+ @force_inclusive = force_inclusive
30
+
31
+ # FIXME: Remove below when lengthOfStayQuantity unit is fixed
32
+ @unit = 'd' if @unit == 'days'
33
+ end
34
+
35
+ def inclusive?
36
+ # If the high and low value are at any time the same, then it must be an inclusive value.
37
+ equivalent = attr_val('../cda:low/@value') == attr_val('../cda:high/@value')
38
+
39
+ # If and inclusivity value is set for any specific value, then mark the value as inclusive.
40
+ # IDEA: This could be limited in future iterations by including the parent type (temporal reference, subset code,
41
+ # etc.)
42
+ inclusive_temporal_ref? || inclusive_length_of_stay? || inclusive_basic_values? || inclusive_subsets? ||
43
+ equivalent || @force_inclusive
44
+ end
45
+
46
+ # Check whether the temporal reference should be marked as inclusive
47
+ def inclusive_temporal_ref?
48
+ # FIXME: NINF is used instead of 0 sometimes...? (not in the IG)
49
+ # FIXME: Given nullFlavor, but IG uses it and nullValue everywhere...
50
+ less_than_equal_tr = attr_val('../@highClosed') == 'true' &&
51
+ (attr_val('../cda:low/@value') == '0' || attr_val('../cda:low/@nullFlavor') == 'NINF')
52
+ greater_than_equal_tr = attr_val('../cda:high/@nullFlavor') == 'PINF' &&
53
+ attr_val('../cda:low/@value')
54
+ # Both less and greater require lowClosed to be set to true
55
+ (less_than_equal_tr || greater_than_equal_tr) && attr_val('../@lowClosed') == 'true'
56
+ end
57
+
58
+ # Check whether the length of stay should be inclusive.
59
+ def inclusive_length_of_stay?
60
+ # lengthOfStay - EH111, EH108
61
+ less_than_equal_los = attr_val('../cda:low/@nullFlavor') == 'NINF' &&
62
+ attr_val('../@highClosed') != 'false'
63
+
64
+ greater_than_equal_los = attr_val('../cda:high/@nullFlavor') == 'PINF' &&
65
+ attr_val('../@lowClosed') != 'false'
66
+ # Both less and greater require that the type is PQ
67
+ (less_than_equal_los || greater_than_equal_los) && attr_val('@xsi:type') == 'PQ'
68
+ end
69
+
70
+ # Check is the basic values should be marked as inclusive, currently only checks for greater than case
71
+ def inclusive_basic_values?
72
+ # Basic values - EP65, EP9, and more
73
+ attr_val('../cda:high/@nullFlavor') == 'PINF' &&
74
+ attr_val('../cda:low/@value') &&
75
+ attr_val('../@lowClosed') != 'false' &&
76
+ attr_val('../@xsi:type') == 'IVL_PQ'
77
+ end
78
+
79
+ # Check if subset values should be marked as inclusive. Currently only handles greater than
80
+ def inclusive_subsets?
81
+ # subset - EP128, EH108
82
+ attr_val('../cda:low/@value') != '0' &&
83
+ !attr_val('../cda:high/@value') &&
84
+ attr_val('../@lowClosed') != 'false' &&
85
+ !attr_val('../../../../../qdm:subsetCode/@code').nil?
86
+ end
87
+
88
+ def derived?
89
+ case attr_val('./@nullFlavor')
90
+ when 'DER'
91
+ true
92
+ else
93
+ false
94
+ end
95
+ end
96
+
97
+ def expression
98
+ if !derived?
99
+ nil
100
+ else
101
+ attr_val('./cda:expression/@value')
102
+ end
103
+ end
104
+
105
+ # Generates this classes hqmf-model equivalent
106
+ def to_model
107
+ HQMF::Value.new(type, unit, value, inclusive?, derived?, expression)
108
+ end
109
+ end
110
+
111
+ # Represents a HQMF physical quantity which can have low and high bounds
112
+ class Range
113
+ include HQMF2::Utilities
114
+ attr_accessor :low, :high, :width
115
+
116
+ def initialize(entry, type = nil)
117
+ @type = type
118
+ @entry = entry
119
+ return unless @entry
120
+ @low = optional_value("#{default_element_name}/cda:low", default_bounds_type)
121
+ @high = optional_value("#{default_element_name}/cda:high", default_bounds_type)
122
+ # Unset low bound to resolve verbose value bounds descriptions
123
+ @low = nil if (@high.try(:value) && @high.value.to_i > 0) && (@low.try(:value) && @low.value.try(:to_i) == 0)
124
+ @width = optional_value("#{default_element_name}/cda:width", 'PQ')
125
+ end
126
+
127
+ def type
128
+ @type || attr_val('./@xsi:type')
129
+ end
130
+
131
+ # Generates this classes hqmf-model equivalent
132
+ def to_model
133
+ lm = low.try(:to_model)
134
+ hm = high.try(:to_model)
135
+ wm = width.try(:to_model)
136
+ model_type = type
137
+ if @entry.at_xpath('./cda:uncertainRange', HQMF2::Document::NAMESPACES)
138
+ model_type = 'IVL_PQ'
139
+ end
140
+
141
+ if generate_any_value?(lm, hm)
142
+ # Generate AnyValue if the only elements in the range are AnyValues.
143
+ HQMF::AnyValue.new
144
+ elsif generate_value?(lm, hm)
145
+ # Generate a singel value if both low and high are the same
146
+ HQMF::Value.new(lm.type, nil, lm.value, lm.inclusive?, lm.derived?, lm.expression)
147
+ else
148
+ HQMF::Range.new(model_type, lm, hm, wm)
149
+ end
150
+ end
151
+
152
+ # Check if are only AnyValue elements for low and high
153
+ def generate_any_value?(lm, hm)
154
+ (lm.nil? || lm.is_a?(HQMF::AnyValue)) && (hm.nil? || hm.is_a?(HQMF::AnyValue))
155
+ end
156
+
157
+ # Check if the value for the range should actually produce a single value instead of a range (if low and high are
158
+ # the same)
159
+ def generate_value?(lm, hm)
160
+ !lm.nil? && lm.try(:value) == hm.try(:value) && lm.try(:unit).nil? && hm.try(:unit).nil?
161
+ end
162
+
163
+ private
164
+
165
+ # Either derives a value from a specific path or generates a new value (or returns nil if none found)
166
+ def optional_value(xpath, type)
167
+ value_def = @entry.at_xpath(xpath, HQMF2::Document::NAMESPACES)
168
+ return unless value_def
169
+ if value_def['flavorId'] == 'ANY.NONNULL'
170
+ AnyValue.new
171
+ else
172
+ created_value = Value.new(value_def, type)
173
+ # Return nil if no value was parsed
174
+ created_value if created_value.try(:value)
175
+ end
176
+ end
177
+
178
+ # Defines how the time based element should be described
179
+ def default_element_name
180
+ case type
181
+ when 'IVL_PQ'
182
+ '.'
183
+ when 'IVL_TS'
184
+ 'cda:phase'
185
+ else
186
+ 'cda:uncertainRange'
187
+ end
188
+ end
189
+
190
+ # Sets up the default bound type as either time based or a physical quantity
191
+ def default_bounds_type
192
+ case type
193
+ when 'IVL_TS'
194
+ 'TS'
195
+ else
196
+ 'PQ'
197
+ end
198
+ end
199
+ end
200
+
201
+ # Represents an HQMF effective time which is a specialization of an interval
202
+ class EffectiveTime < Range
203
+ def initialize(entry)
204
+ super
205
+ end
206
+
207
+ def type
208
+ 'IVL_TS'
209
+ end
210
+ end
211
+
212
+ # Represents a HQMF CD value which has a code and codeSystem
213
+ class Coded
214
+ include HQMF2::Utilities
215
+
216
+ def initialize(entry)
217
+ @entry = entry
218
+ end
219
+
220
+ def type
221
+ attr_val('./@xsi:type') || 'CD'
222
+ end
223
+
224
+ def system
225
+ attr_val('./@codeSystem')
226
+ end
227
+
228
+ def code
229
+ attr_val('./@code')
230
+ end
231
+
232
+ def code_list_id
233
+ attr_val('./@valueSet')
234
+ end
235
+
236
+ def title
237
+ attr_val('./*/@value')
238
+ end
239
+
240
+ def value
241
+ code
242
+ end
243
+
244
+ def derived?
245
+ false
246
+ end
247
+
248
+ def unit
249
+ nil
250
+ end
251
+
252
+ # Generates this classes hqmf-model equivalent
253
+ def to_model
254
+ HQMF::Coded.new(type, system, code, code_list_id, title)
255
+ end
256
+ end
257
+ # Represents a subset of a specific group (the first in the group, the sum of the group, etc.)
258
+ class SubsetOperator
259
+ include HQMF2::Utilities
260
+
261
+ attr_reader :type, :value
262
+ ORDER_SUBSETS = %w(FIRST SECOND THIRD FOURTH FIFTH)
263
+ LAST_SUBSETS = %w(LAST RECENT)
264
+ TIME_SUBSETS = %w(DATEDIFF TIMEDIFF)
265
+ QDM_TYPE_MAP = { 'QDM_LAST:' => 'RECENT', 'QDM_SUM:SUM' => 'COUNT' }
266
+
267
+ def initialize(entry)
268
+ @entry = entry
269
+
270
+ sequence_number = attr_val('./cda:sequenceNumber/@value')
271
+ qdm_subset_code = attr_val('./qdm:subsetCode/@code')
272
+ subset_code = attr_val('./cda:subsetCode/@code')
273
+ if sequence_number
274
+ @type = ORDER_SUBSETS[sequence_number.to_i - 1]
275
+ else
276
+ @type = translate_type(subset_code, qdm_subset_code)
277
+ end
278
+
279
+ value_def = handle_value_definition
280
+ @value = HQMF2::Range.new(value_def, 'IVL_PQ') if value_def && !@value
281
+ end
282
+
283
+ # Return the value definition (what to calculate it on) associated with this subset.
284
+ # Other values, such as type and value, may be modified depending on this value.
285
+ def handle_value_definition
286
+ value_def = @entry.at_xpath('./*/cda:repeatNumber', HQMF2::Document::NAMESPACES)
287
+ unless value_def
288
+ # TODO: HQMF needs better differentiation between SUM & COUNT...
289
+ # currently using presence of repeatNumber...
290
+ @type = 'SUM' if @type == 'COUNT'
291
+ value_def = @entry.at_xpath('./*/cda:value', HQMF2::Document::NAMESPACES)
292
+ end
293
+
294
+ # TODO: Resolve extracting values embedded in criteria within outboundRel's
295
+ if @type == 'SUM'
296
+ value_def = @entry.at_xpath('./*/*/*/cda:value', HQMF2::Document::NAMESPACES)
297
+ end
298
+
299
+ if value_def
300
+ value_type = value_def.at_xpath('./@xsi:type', HQMF2::Document::NAMESPACES)
301
+ @value = HQMF2::AnyValue.new if String.try_convert(value_type) == 'ANY'
302
+ end
303
+
304
+ value_def
305
+ end
306
+
307
+ # Take a qdm type code to map it to a subset operator, or failing at finding that, return the given subset code.
308
+ def translate_type(subset_code, qdm_subset_code)
309
+ combined = "#{qdm_subset_code}:#{subset_code}"
310
+ if QDM_TYPE_MAP[combined]
311
+ QDM_TYPE_MAP[combined]
312
+ else
313
+ subset_code
314
+ end
315
+ end
316
+
317
+ # Generates this classes hqmf-model equivalent
318
+ def to_model
319
+ vm = value ? value.to_model : nil
320
+ HQMF::SubsetOperator.new(type, vm)
321
+ end
322
+ end
323
+
324
+ # Represents a time bounded reference. Wraps the "Range" class
325
+ class TemporalReference
326
+ include HQMF2::Utilities
327
+
328
+ attr_reader :type, :reference, :range
329
+
330
+ # Use updated mappings to HDS temporal reference types (as used in SimpleXML Parser)
331
+ # https://github.com/projecttacoma/simplexml_parser/blob/fa0f589d98059b88d77dc3cb465b62184df31671/lib/model/types.rb#L167
332
+ UPDATED_TYPES = {
333
+ 'EAOCW' => 'EACW',
334
+ 'EAEORECW' => 'EACW',
335
+ 'EAOCWSO' => 'EACWS',
336
+ 'EASORECWS' => 'EACWS',
337
+ 'EBOCW' => 'EBCW',
338
+ 'EBEORECW' => 'EBCW',
339
+ 'EBOCWSO' => 'EBCWS',
340
+ 'EBSORECWS' => 'EBCWS',
341
+ 'ECWSO' => 'ECWS',
342
+ 'SAOCWEO' => 'SACWE',
343
+ 'SAEORSCWE' => 'SACWE',
344
+ 'SAOCW' => 'SACW',
345
+ 'SASORSCW' => 'SACW',
346
+ 'SBOCWEO' => 'SBCWE',
347
+ 'SBEORSCWE' => 'SBCWE',
348
+ 'SBOCW' => 'SBCW',
349
+ 'SBSORSCW' => 'SBCW',
350
+ 'SCWEO' => 'SCWE',
351
+ 'OVERLAPS' => 'OVERLAP'
352
+ }
353
+
354
+ def initialize(entry)
355
+ @entry = entry
356
+ @type = UPDATED_TYPES[attr_val('./@typeCode')] || attr_val('./@typeCode')
357
+ @reference = Reference.new(@entry.at_xpath('./*/cda:id', HQMF2::Document::NAMESPACES))
358
+ range_def = @entry.at_xpath('./qdm:temporalInformation/qdm:delta', HQMF2::Document::NAMESPACES)
359
+ @range = HQMF2::Range.new(range_def, 'IVL_PQ') if range_def
360
+ end
361
+
362
+ # Generates this classes hqmf-model equivalent
363
+ def to_model
364
+ rm = range ? range.to_model : nil
365
+ HQMF::TemporalReference.new(type, reference.to_model, rm)
366
+ end
367
+ end
368
+
369
+ # Represents a HQMF reference to a data criteria that has a given type
370
+ class TypedReference
371
+ include HQMF2::Utilities
372
+ attr_accessor :id, :type, :mood
373
+
374
+ # Create a new HQMF::Reference
375
+ # @param [String] id
376
+ def initialize(entry, type = nil, verbose = false)
377
+ @entry = entry
378
+ @type = type || attr_val('./@classCode')
379
+ @mood = attr_val('./@moodCode')
380
+ @entry = entry.elements.first unless entry.at_xpath('./@extension')
381
+ @verbose = verbose
382
+ end
383
+
384
+ # Generate the reference for the typed reference to use
385
+ def reference
386
+ value = "#{attr_val('./@extension')}_#{attr_val('./@root')}"
387
+ strip_tokens(value)
388
+ end
389
+
390
+ # Generates this classes hqmf-model equivalent
391
+ def to_model
392
+ HQMF::TypedReference.new(reference, @type, @mood)
393
+ end
394
+ end
395
+
396
+ # Represents a HQMF reference from a precondition to a data criteria
397
+ class Reference
398
+ include HQMF2::Utilities
399
+
400
+ def initialize(entry)
401
+ @entry = entry
402
+ end
403
+
404
+ # Generates the id to use for a reference
405
+ def id
406
+ if @entry.is_a? String
407
+ @entry
408
+ else
409
+ id = strip_tokens("#{attr_val('./@extension')}_#{attr_val('./@root')}")
410
+ # Handle MeasurePeriod references for calculation code
411
+ id = 'MeasurePeriod' if id.try(:start_with?, 'measureperiod')
412
+ id
413
+ end
414
+ end
415
+
416
+ # Generates this classes hqmf-model equivalent
417
+ def to_model
418
+ HQMF::Reference.new(id)
419
+ end
420
+ end
421
+
422
+ # Creates a Data Criteria given a map of options, and is used when full
423
+ # criteria parsing is not necessary.
424
+ class DataCriteriaWrapper
425
+ attr_accessor :status, :value, :effective_time
426
+ attr_accessor :temporal_references, :subset_operators, :children_criteria
427
+ attr_accessor :derivation_operator, :negation, :negation_code_list_id, :description
428
+ attr_accessor :field_values, :source_data_criteria, :specific_occurrence_const
429
+ attr_accessor :specific_occurrence, :comments
430
+ attr_accessor :id, :title, :definition, :variable, :code_list_id, :value, :inline_code_list
431
+
432
+ def initialize(opts = {})
433
+ opts.each { |k, v| instance_variable_set("@#{k}", v) }
434
+ end
435
+
436
+ # Generates this classes hqmf-model equivalent
437
+ def to_model
438
+ mv = @value ? @value.to_model : nil
439
+ met = @effective_time ? @effective_time.to_model : nil
440
+ mtr = @temporal_references
441
+ mso = @subset_operators
442
+ HQMF::DataCriteria.new(@id, @title, nil, @description, @code_list_id, @children_criteria,
443
+ @derivation_operator, @definition, @status, mv, field_values, met, @inline_code_list,
444
+ @negation, @negation_code_list_id, mtr, mso, @specific_occurrence,
445
+ @specific_occurrence_const, @source_data_criteria, @comments, @variable)
446
+ end
447
+ end
448
+ end