module HQMF2 # Used to represent 'any value' in criteria that require a value be present but # don't specify any restrictions on that value class AnyValue attr_reader :type def initialize(type = 'ANYNonNull') @type = type end # Generates this classes hqmf-model equivalent def to_model HQMF::AnyValue.new(@type) end end # Represents a bound within a HQMF pauseQuantity, has a value, a unit and an # inclusive/exclusive indicator class Value include HQMF2::Utilities attr_reader :type, :unit, :value def initialize(entry, default_type = 'PQ', force_inclusive = false, _parent = nil) @entry = entry @type = attr_val('./@xsi:type') || default_type @unit = attr_val('./@unit') @value = attr_val('./@value') @force_inclusive = force_inclusive # FIXME: Remove below when lengthOfStayQuantity unit is fixed @unit = 'd' if @unit == 'days' end def inclusive? # If the high and low value are at any time the same, then it must be an inclusive value. equivalent = attr_val('../cda:low/@value') == attr_val('../cda:high/@value') # If and inclusivity value is set for any specific value, then mark the value as inclusive. # IDEA: This could be limited in future iterations by including the parent type (temporal reference, subset code, # etc.) inclusive_temporal_ref? || inclusive_length_of_stay? || inclusive_basic_values? || inclusive_subsets? || equivalent || @force_inclusive end # Check whether the temporal reference should be marked as inclusive def inclusive_temporal_ref? # FIXME: NINF is used instead of 0 sometimes...? (not in the IG) # FIXME: Given nullFlavor, but IG uses it and nullValue everywhere... less_than_equal_tr = attr_val('../@highClosed') == 'true' && (attr_val('../cda:low/@value') == '0' || attr_val('../cda:low/@nullFlavor') == 'NINF') greater_than_equal_tr = attr_val('../cda:high/@nullFlavor') == 'PINF' && attr_val('../cda:low/@value') # Both less and greater require lowClosed to be set to true (less_than_equal_tr || greater_than_equal_tr) && attr_val('../@lowClosed') == 'true' end # Check whether the length of stay should be inclusive. def inclusive_length_of_stay? # lengthOfStay - EH111, EH108 less_than_equal_los = attr_val('../cda:low/@nullFlavor') == 'NINF' && attr_val('../@highClosed') != 'false' greater_than_equal_los = attr_val('../cda:high/@nullFlavor') == 'PINF' && attr_val('../@lowClosed') != 'false' # Both less and greater require that the type is PQ (less_than_equal_los || greater_than_equal_los) && attr_val('@xsi:type') == 'PQ' end # Check is the basic values should be marked as inclusive, currently only checks for greater than case def inclusive_basic_values? # Basic values - EP65, EP9, and more attr_val('../cda:high/@nullFlavor') == 'PINF' && attr_val('../cda:low/@value') && attr_val('../@lowClosed') != 'false' && attr_val('../@xsi:type') == 'IVL_PQ' end # Check if subset values should be marked as inclusive. Currently only handles greater than def inclusive_subsets? # subset - EP128, EH108 attr_val('../cda:low/@value') != '0' && !attr_val('../cda:high/@value') && attr_val('../@lowClosed') != 'false' && !attr_val('../../../../../qdm:subsetCode/@code').nil? end def derived? case attr_val('./@nullFlavor') when 'DER' true else false end end def expression if !derived? nil else attr_val('./cda:expression/@value') end end # Generates this classes hqmf-model equivalent def to_model HQMF::Value.new(type, unit, value, inclusive?, derived?, expression) end end # Represents a HQMF physical quantity which can have low and high bounds class Range include HQMF2::Utilities attr_accessor :low, :high, :width def initialize(entry, type = nil) @type = type @entry = entry return unless @entry @low = optional_value("#{default_element_name}/cda:low", default_bounds_type) @high = optional_value("#{default_element_name}/cda:high", default_bounds_type) # Unset low bound to resolve verbose value bounds descriptions @low = nil if (@high.try(:value) && @high.value.to_i > 0) && (@low.try(:value) && @low.value.try(:to_i) == 0) @width = optional_value("#{default_element_name}/cda:width", 'PQ') end def type @type || attr_val('./@xsi:type') end # Generates this classes hqmf-model equivalent def to_model lm = low.try(:to_model) hm = high.try(:to_model) wm = width.try(:to_model) model_type = type if @entry.at_xpath('./cda:uncertainRange', HQMF2::Document::NAMESPACES) model_type = 'IVL_PQ' end if generate_any_value?(lm, hm) # Generate AnyValue if the only elements in the range are AnyValues. HQMF::AnyValue.new elsif generate_value?(lm, hm) # Generate a singel value if both low and high are the same HQMF::Value.new(lm.type, nil, lm.value, lm.inclusive?, lm.derived?, lm.expression) else HQMF::Range.new(model_type, lm, hm, wm) end end # Check if are only AnyValue elements for low and high def generate_any_value?(lm, hm) (lm.nil? || lm.is_a?(HQMF::AnyValue)) && (hm.nil? || hm.is_a?(HQMF::AnyValue)) end # Check if the value for the range should actually produce a single value instead of a range (if low and high are # the same) def generate_value?(lm, hm) !lm.nil? && lm.try(:value) == hm.try(:value) && lm.try(:unit).nil? && hm.try(:unit).nil? end private # Either derives a value from a specific path or generates a new value (or returns nil if none found) def optional_value(xpath, type) value_def = @entry.at_xpath(xpath, HQMF2::Document::NAMESPACES) return unless value_def if value_def['flavorId'] == 'ANY.NONNULL' AnyValue.new else created_value = Value.new(value_def, type) # Return nil if no value was parsed created_value if created_value.try(:value) end end # Defines how the time based element should be described def default_element_name case type when 'IVL_PQ' '.' when 'IVL_TS' 'cda:phase' else 'cda:uncertainRange' end end # Sets up the default bound type as either time based or a physical quantity def default_bounds_type case type when 'IVL_TS' 'TS' else 'PQ' end end end # Represents an HQMF effective time which is a specialization of an interval class EffectiveTime < Range def initialize(entry) super end def type 'IVL_TS' end end # Represents a HQMF CD value which has a code and codeSystem class Coded include HQMF2::Utilities def initialize(entry) @entry = entry end def type attr_val('./@xsi:type') || 'CD' end def system attr_val('./@codeSystem') end def code attr_val('./@code') end def code_list_id attr_val('./@valueSet') end def title attr_val('./*/@value') end def value code end def derived? false end def unit nil end # Generates this classes hqmf-model equivalent def to_model HQMF::Coded.new(type, system, code, code_list_id, title) end end # Represents a subset of a specific group (the first in the group, the sum of the group, etc.) class SubsetOperator include HQMF2::Utilities attr_reader :type, :value ORDER_SUBSETS = %w(FIRST SECOND THIRD FOURTH FIFTH) LAST_SUBSETS = %w(LAST RECENT) TIME_SUBSETS = %w(DATEDIFF TIMEDIFF) QDM_TYPE_MAP = { 'QDM_LAST:' => 'RECENT', 'QDM_SUM:SUM' => 'COUNT' } def initialize(entry) @entry = entry sequence_number = attr_val('./cda:sequenceNumber/@value') qdm_subset_code = attr_val('./qdm:subsetCode/@code') subset_code = attr_val('./cda:subsetCode/@code') if sequence_number @type = ORDER_SUBSETS[sequence_number.to_i - 1] else @type = translate_type(subset_code, qdm_subset_code) end value_def = handle_value_definition @value = HQMF2::Range.new(value_def, 'IVL_PQ') if value_def && !@value end # Return the value definition (what to calculate it on) associated with this subset. # Other values, such as type and value, may be modified depending on this value. def handle_value_definition value_def = @entry.at_xpath('./*/cda:repeatNumber', HQMF2::Document::NAMESPACES) unless value_def # TODO: HQMF needs better differentiation between SUM & COUNT... # currently using presence of repeatNumber... @type = 'SUM' if @type == 'COUNT' value_def = @entry.at_xpath('./*/cda:value', HQMF2::Document::NAMESPACES) end # TODO: Resolve extracting values embedded in criteria within outboundRel's if @type == 'SUM' value_def = @entry.at_xpath('./*/*/*/cda:value', HQMF2::Document::NAMESPACES) end if value_def value_type = value_def.at_xpath('./@xsi:type', HQMF2::Document::NAMESPACES) @value = HQMF2::AnyValue.new if String.try_convert(value_type) == 'ANY' end value_def end # Take a qdm type code to map it to a subset operator, or failing at finding that, return the given subset code. def translate_type(subset_code, qdm_subset_code) combined = "#{qdm_subset_code}:#{subset_code}" if QDM_TYPE_MAP[combined] QDM_TYPE_MAP[combined] else subset_code end end # Generates this classes hqmf-model equivalent def to_model vm = value ? value.to_model : nil HQMF::SubsetOperator.new(type, vm) end end # Represents a time bounded reference. Wraps the "Range" class class TemporalReference include HQMF2::Utilities attr_reader :type, :reference, :range # Use updated mappings to HDS temporal reference types (as used in SimpleXML Parser) # https://github.com/projecttacoma/simplexml_parser/blob/fa0f589d98059b88d77dc3cb465b62184df31671/lib/model/types.rb#L167 UPDATED_TYPES = { 'EAOCW' => 'EACW', 'EAEORECW' => 'EACW', 'EAOCWSO' => 'EACWS', 'EASORECWS' => 'EACWS', 'EBOCW' => 'EBCW', 'EBEORECW' => 'EBCW', 'EBOCWSO' => 'EBCWS', 'EBSORECWS' => 'EBCWS', 'ECWSO' => 'ECWS', 'SAOCWEO' => 'SACWE', 'SAEORSCWE' => 'SACWE', 'SAOCW' => 'SACW', 'SASORSCW' => 'SACW', 'SBOCWEO' => 'SBCWE', 'SBEORSCWE' => 'SBCWE', 'SBOCW' => 'SBCW', 'SBSORSCW' => 'SBCW', 'SCWEO' => 'SCWE', 'OVERLAPS' => 'OVERLAP' } def initialize(entry) @entry = entry @type = UPDATED_TYPES[attr_val('./@typeCode')] || attr_val('./@typeCode') @reference = Reference.new(@entry.at_xpath('./*/cda:id', HQMF2::Document::NAMESPACES)) range_def = @entry.at_xpath('./qdm:temporalInformation/qdm:delta', HQMF2::Document::NAMESPACES) @range = HQMF2::Range.new(range_def, 'IVL_PQ') if range_def end # Generates this classes hqmf-model equivalent def to_model rm = range ? range.to_model : nil HQMF::TemporalReference.new(type, reference.to_model, rm) end end # Represents a HQMF reference to a data criteria that has a given type class TypedReference include HQMF2::Utilities attr_accessor :id, :type, :mood # Create a new HQMF::Reference # @param [String] id def initialize(entry, type = nil, verbose = false) @entry = entry @type = type || attr_val('./@classCode') @mood = attr_val('./@moodCode') @entry = entry.elements.first unless entry.at_xpath('./@extension') @verbose = verbose end # Generate the reference for the typed reference to use def reference value = "#{attr_val('./@extension')}_#{attr_val('./@root')}" strip_tokens(value) end # Generates this classes hqmf-model equivalent def to_model HQMF::TypedReference.new(reference, @type, @mood) end end # Represents a HQMF reference from a precondition to a data criteria class Reference include HQMF2::Utilities def initialize(entry) @entry = entry end # Generates the id to use for a reference def id if @entry.is_a? String @entry else id = strip_tokens("#{attr_val('./@extension')}_#{attr_val('./@root')}") # Handle MeasurePeriod references for calculation code id = 'MeasurePeriod' if id.try(:start_with?, 'measureperiod') id end end # Generates this classes hqmf-model equivalent def to_model HQMF::Reference.new(id) end end # Creates a Data Criteria given a map of options, and is used when full # criteria parsing is not necessary. class DataCriteriaWrapper attr_accessor :status, :value, :effective_time attr_accessor :temporal_references, :subset_operators, :children_criteria attr_accessor :derivation_operator, :negation, :negation_code_list_id, :description attr_accessor :field_values, :source_data_criteria, :specific_occurrence_const attr_accessor :specific_occurrence, :comments attr_accessor :id, :title, :definition, :variable, :code_list_id, :value, :inline_code_list def initialize(opts = {}) opts.each { |k, v| instance_variable_set("@#{k}", v) } end # Generates this classes hqmf-model equivalent def to_model mv = @value ? @value.to_model : nil met = @effective_time ? @effective_time.to_model : nil mtr = @temporal_references mso = @subset_operators HQMF::DataCriteria.new(@id, @title, nil, @description, @code_list_id, @children_criteria, @derivation_operator, @definition, @status, mv, field_values, met, @inline_code_list, @negation, @negation_code_list_id, mtr, mso, @specific_occurrence, @specific_occurrence_const, @source_data_criteria, @comments, @variable) end end end