projectcypress/health-data-standards

View on GitHub
lib/hqmf-parser/2.0/types.rb

Summary

Maintainability
B
4 hrs
Test Coverage
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