health-data-standards 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/Gemfile +5 -2
  2. data/Rakefile +6 -1
  3. data/VERSION +1 -1
  4. data/lib/health-data-standards.rb +27 -2
  5. data/lib/health-data-standards/export/view_helper.rb +15 -16
  6. data/lib/health-data-standards/ext/string.rb +5 -0
  7. data/lib/health-data-standards/ext/symbol.rb +8 -0
  8. data/lib/health-data-standards/import/c32/allergy_importer.rb +42 -0
  9. data/lib/health-data-standards/import/c32/encounter_importer.rb +80 -0
  10. data/lib/health-data-standards/import/c32/immunization_importer.rb +61 -0
  11. data/lib/health-data-standards/import/c32/medication_importer.rb +138 -0
  12. data/lib/health-data-standards/import/c32/patient_importer.rb +139 -0
  13. data/lib/health-data-standards/import/c32/procedure_importer.rb +55 -0
  14. data/lib/health-data-standards/import/c32/result_importer.rb +58 -0
  15. data/lib/health-data-standards/import/c32/section_importer.rb +214 -0
  16. data/lib/health-data-standards/import/c32/vital_sign_importer.rb +12 -0
  17. data/lib/health-data-standards/models/allergy.rb +4 -0
  18. data/lib/health-data-standards/models/encounter.rb +7 -0
  19. data/lib/health-data-standards/models/entry.rb +131 -3
  20. data/lib/health-data-standards/models/fulfillment_history.rb +11 -0
  21. data/lib/health-data-standards/models/immunization.rb +5 -0
  22. data/lib/health-data-standards/models/lab_result.rb +4 -0
  23. data/lib/health-data-standards/models/medication.rb +25 -0
  24. data/lib/health-data-standards/models/order_information.rb +9 -0
  25. data/lib/health-data-standards/models/procedure.rb +4 -0
  26. data/lib/health-data-standards/models/record.rb +1 -0
  27. data/lib/health-data-standards/util/code_system_helper.rb +41 -0
  28. data/lib/health-data-standards/util/hl7_helper.rb +25 -0
  29. data/templates/_allergies.c32.erb +3 -1
  30. data/templates/_care_goals.c32.erb +1 -1
  31. data/templates/_code_with_reference.c32.erb +10 -5
  32. data/templates/_conditions.c32.erb +2 -2
  33. data/templates/_encounters.c32.erb +2 -1
  34. data/templates/_immunizations.c32.erb +2 -1
  35. data/templates/_medical_equipment.c32.erb +1 -1
  36. data/templates/_medications.c32.erb +2 -1
  37. data/templates/_narrative_block.c32.erb +14 -0
  38. data/templates/_procedures.c32.erb +2 -1
  39. data/templates/_results.c32.erb +2 -2
  40. data/templates/_social_history.c32.erb +1 -1
  41. data/templates/_vital_signs.c32.erb +2 -2
  42. metadata +37 -16
@@ -0,0 +1,12 @@
1
+ module HealthDataStandards
2
+ module Import
3
+ module C32
4
+ class VitalSignImporter < ResultImporter
5
+ def initialize
6
+ super
7
+ @entry_xpath = "//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.14']"
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ class Allergy < Entry
2
+ field :reaction, type: Hash
3
+ field :severity, type: Hash
4
+ end
@@ -0,0 +1,7 @@
1
+ class Encounter < Entry
2
+ field :performer, type: Hash
3
+ field :facility, type: Hash
4
+ field :admitType, type: Hash, as: 'admit_type'
5
+ field :dischargeDisp, type: Hash, as: 'discharge_disp'
6
+ embeds_one :reason, class_name: "Entry"
7
+ end
@@ -8,9 +8,9 @@ class Entry
8
8
  field :time, type: Integer
9
9
  field :start_time, type: Integer
10
10
  field :end_time, type: Integer
11
- field :status, type: Symbol
12
- field :codes, type: Hash
13
- field :value, type: Hash
11
+ field :status, type: String
12
+ field :codes, type: Hash, default: {}
13
+ field :value, type: Hash, default: {}
14
14
 
15
15
  def single_code_value?
16
16
  codes.size == 1 && codes.first[1].size == 1
@@ -20,6 +20,32 @@ class Entry
20
20
  codes.map {|code_set, codes| "#{code_set}: #{codes.join(', ')}"}.join(' ')
21
21
  end
22
22
 
23
+ # Will return a single code and code set if one exists in the code sets that are
24
+ # passed in. Returns a hash with a key of code and code_set if found, nil otherwise
25
+ def preferred_code(preferred_code_sets)
26
+ matching_code_sets = preferred_code_sets & codes.keys
27
+ if matching_code_sets.present?
28
+ code_set = matching_code_sets.first
29
+ {'code' => codes[code_set].first, 'code_set' => code_set}
30
+ else
31
+ nil
32
+ end
33
+ end
34
+
35
+ # Will return an Array of code and code_set hashes for all codes for this entry
36
+ # except for the preferred_code. It is intended that these codes would be used in
37
+ # the translation elements as childern of a CDA code element
38
+ def translation_codes(preferred_code_sets)
39
+ tx_codes = []
40
+ codes.each_pair do |code_set, code_list|
41
+ code_list.each do |code|
42
+ tx_codes << {'code' => code, 'code_set' => code_set}
43
+ end
44
+ end
45
+
46
+ tx_codes - [preferred_code(preferred_code_sets)]
47
+ end
48
+
23
49
  def times_to_s
24
50
  if start_time.present? || end_time.present?
25
51
  start_string = start_time ? Time.at(start_time).to_formatted_s(:long_ordinal) : 'UNK'
@@ -48,4 +74,106 @@ class Entry
48
74
  end
49
75
  end
50
76
  end
77
+
78
+ def self.from_event_hash(event)
79
+ entry = Entry.new
80
+ if event['code']
81
+ entry.add_code(event['code'], event['code_set'])
82
+ end
83
+ entry.time = event['time']
84
+ if event['value']
85
+ entry.set_value(event['value'], event['unit'])
86
+ end
87
+ if event['description']
88
+ entry.description = event['description']
89
+ end
90
+ if event['status']
91
+ entry.status = event['status']
92
+ end
93
+ entry
94
+ end
95
+
96
+ # Add a code into the Entry
97
+ # @param [String] code the code to add
98
+ # @param [String] code_system the code system that the code belongs to
99
+ def add_code(code, code_system)
100
+ self.codes[code_system] ||= []
101
+ self.codes[code_system] << code
102
+ end
103
+
104
+ # Sets the value for the entry
105
+ # @param [String] scalar the value
106
+ # @param [String] units the units of the scalar value
107
+ def set_value(scalar, units=nil)
108
+ self.value[:scalar] = scalar
109
+ self.value[:units] = units
110
+ end
111
+
112
+ # Checks if a code is in the list of possible codes
113
+ # @param [Array] code_set an Array of Hashes that describe the values for code sets
114
+ # @return [true, false] whether the code is in the list of desired codes
115
+ def is_in_code_set?(code_set)
116
+ codes.keys.each do |code_system|
117
+ all_codes_in_system = code_set.find_all {|set| set['set'] == code_system}
118
+ all_codes_in_system.each do |codes_in_system|
119
+ matching_codes = codes_in_system['values'] & codes[code_system]
120
+ if matching_codes.length > 0
121
+ return true
122
+ end
123
+ end
124
+ end
125
+ false
126
+ end
127
+
128
+ # Tries to find a single point in time for this entry. Will first return time if it is present,
129
+ # then fall back to start_time and finally end_time
130
+ def as_point_in_time
131
+ if time
132
+ time
133
+ elsif start_time
134
+ start_time
135
+ else
136
+ end_time
137
+ end
138
+ end
139
+
140
+ # Checks to see if this Entry can be used as a date range
141
+ # @return [true, false] If the Entry has a start and end time returns true, false otherwise.
142
+ def is_date_range?
143
+ start_time.present? && end_time.present?
144
+ end
145
+
146
+ # Checks to see if this Entry is usable for measure calculation. This means that it contains
147
+ # at least one code and has one of its time properties set (start, end or time)
148
+ # @return [true, false]
149
+ def usable?
150
+ codes.present? && (start_time.present? || end_time.present? || time.present?)
151
+ end
152
+
153
+ # Creates a Hash for this Entry
154
+ # @return [Hash] a Hash representing the Entry
155
+ def to_hash
156
+ entry_hash = {}
157
+ entry_hash['codes'] = codes
158
+ unless value.empty?
159
+ entry_hash['value'] = value
160
+ end
161
+
162
+ if is_date_range?
163
+ entry_hash['start_time'] = start_time
164
+ entry_hash['end_time'] = end_time
165
+ else
166
+ entry_hash['time'] = as_point_in_time
167
+ end
168
+
169
+ if status
170
+ entry_hash['status'] = status
171
+ end
172
+
173
+ if description
174
+ entry_hash['description'] = description
175
+ end
176
+
177
+ entry_hash
178
+ end
51
179
  end
@@ -0,0 +1,11 @@
1
+ class FulfillmentHistory
2
+ include Mongoid::Document
3
+
4
+ field :prescriptionNumber, type: String, as: 'prescription_number'
5
+ field :provider, type: Hash
6
+ field :dispensingPharmacyLocation, type: Hash, as: 'dispensing_pharmacy_location'
7
+ field :dispenseDate, type: Integer, as: 'dispense_date'
8
+ field :quantityDispensed, type: Hash, as: 'quantity_dispensed'
9
+ field :fillNumber, type: Integer, as: 'fill_number'
10
+ field :fillStatus, type: Hash, as: 'fill_status'
11
+ end
@@ -0,0 +1,5 @@
1
+ class Immunization < Entry
2
+ field :refusalInd, type: Boolean, as: 'refusal_ind'
3
+ field :performer, type: Hash
4
+ field :refusalReason, type: Hash, as: 'refusal_reason'
5
+ end
@@ -0,0 +1,4 @@
1
+ class LabResult < Entry
2
+ field :referenceRange, type: String, as: 'reference_range'
3
+ field :interpretation, type: Hash
4
+ end
@@ -0,0 +1,25 @@
1
+ class Medication < Entry
2
+ field :administrationTiming, type: Hash, as: 'administration_timing'
3
+ field :freeTextSig, type: String, as: 'free_text_sig'
4
+ field :dose, type: Hash
5
+ field :brandName, type: String, as: 'brand_name'
6
+ field :typeOfMedication, type: Hash, as: 'type_of_medication'
7
+ field :statusOfMedication, type: Hash, as: 'status_of_medication'
8
+ embeds_many :fulfillmentHistory, class_name: 'FulfillmentHistory'
9
+ embeds_many :orderInformation, class_name: 'OrderInformation'
10
+ field :route, type: Hash
11
+ field :site, type: Hash
12
+ field :doseRestriction, type: Hash, as: 'dose_restriction'
13
+ field :fulfillmentInstructions, type: String, as: 'fulfillment_instructions'
14
+ field :indication, type: Hash
15
+ field :productForm, type: Hash, as: 'product_form'
16
+ field :vehicle, type: Hash
17
+ field :reaction, type: Hash
18
+ field :deliveryMethod, type: Hash, as: 'delivery_method'
19
+ field :patientInstructions, type: String, as: 'patient_instructions'
20
+
21
+ alias :fulfillment_history :fulfillmentHistory
22
+ alias :fulfillment_history= :fulfillmentHistory=
23
+ alias :order_information :orderInformation
24
+ alias :order_information= :orderInformation=
25
+ end
@@ -0,0 +1,9 @@
1
+ class OrderInformation
2
+ include Mongoid::Document
3
+
4
+ field :orderNumber, type: String, as: 'order_number'
5
+ field :fills, type: Integer
6
+ field :quantityOrdered, type: Hash, as: 'quantity_ordered'
7
+ field :orderExpirationDateTime, type: Integer, as: 'order_expiration_date_time'
8
+ field :orderDateTime, type: Integer, as: 'order_date_time'
9
+ end
@@ -0,0 +1,4 @@
1
+ class Procedure < Entry
2
+ field :performer, type: Hash
3
+ field :site, type: Hash
4
+ end
@@ -9,6 +9,7 @@ class Record
9
9
  field :race, type: String
10
10
  field :ethnicity, type: String
11
11
  field :test_id, type: BSON::ObjectId
12
+ field :medical_record_number, type: String
12
13
 
13
14
  [:allergies, :care_goals, :conditions, :encounters, :immunizations, :medical_equipment,
14
15
  :medications, :procedures, :results, :social_history, :vital_signs].each do |section|
@@ -0,0 +1,41 @@
1
+ module HealthDataStandards
2
+ module Util
3
+ # General helpers for working with codes and code systems
4
+ class CodeSystemHelper
5
+ CODE_SYSTEMS = {
6
+ '2.16.840.1.113883.6.1' => 'LOINC',
7
+ '2.16.840.1.113883.6.96' => 'SNOMED-CT',
8
+ '2.16.840.1.113883.6.12' => 'CPT',
9
+ #'2.16.840.1.113883.3.88.12.80.32' => 'CPT', # Encounter Type from C32, a subset of CPT
10
+ '2.16.840.1.113883.6.88' => 'RxNorm',
11
+ '2.16.840.1.113883.6.103' => 'ICD-9-CM',
12
+ '2.16.840.1.113883.6.104' => 'ICD-9-CM',
13
+ '2.16.840.1.113883.6.90' => 'ICD-10-CM',
14
+ '2.16.840.1.113883.6.14' => 'HCPCS',
15
+ '2.16.840.1.113883.6.59' => 'CVX',
16
+ '2.16.840.1.113883.5.83' => 'HITSP C80 Observation Status'
17
+ }
18
+
19
+ # Returns the name of a code system given an oid
20
+ # @param [String] oid of a code system
21
+ # @return [String] the name of the code system as described in the measure definition JSON
22
+ def self.code_system_for(oid)
23
+ CODE_SYSTEMS[oid] || "Unknown"
24
+ end
25
+
26
+ # Returns the oid for a code system given a codesystem name
27
+ # @param [String] the name of the code system
28
+ # @return [String] the oid of the code system
29
+ def self.oid_for_code_system(code_system)
30
+ CODE_SYSTEMS.invert[code_system]
31
+ end
32
+
33
+ # Returns the whole map of OIDs to code systems
34
+ # @terurn [Hash] oids as keys, code system names as values
35
+ def self.code_systems
36
+ CODE_SYSTEMS
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,25 @@
1
+ module HealthDataStandards
2
+ module Util
3
+ # General helpers for working with HL7 data types
4
+ class HL7Helper
5
+
6
+ # Converts an HL7 timestamp into an Integer
7
+ # @param [String] timestamp the HL7 timestamp. Expects YYYYMMDD format
8
+ # @return [Integer] Date in seconds since the epoch
9
+ def self.timestamp_to_integer(timestamp)
10
+ if timestamp && timestamp.length >= 4
11
+ year = timestamp[0..3].to_i
12
+ month = timestamp.length >= 6 ? timestamp[4..5].to_i : 1
13
+ day = timestamp.length >= 8 ? timestamp[6..7].to_i : 1
14
+ hour = timestamp.length >= 10 ? timestamp[8..9].to_i : 0
15
+ min = timestamp.length >= 12 ? timestamp[10..11].to_i : 0
16
+ sec = timestamp.length >= 14 ? timestamp[12..13].to_i : 0
17
+
18
+ Time.gm(year, month, day, hour, min, sec).to_i
19
+ else
20
+ nil
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -43,7 +43,9 @@
43
43
  <participant typeCode="CSM">
44
44
  <participantRole classCode="MANU">
45
45
  <playingEntity classCode="MMAT">
46
- <%== render :partial => 'code_with_reference', :locals => {:entry => entry, :i => i, :section => 'allergies'} %>
46
+ <%== render :partial => 'code_with_reference',
47
+ :locals => {:entry => entry, :i => i, :section => 'allergies',
48
+ :preferred_code_sets => ['RxNorm']} %>
47
49
  <name><%= entry.description %></name>
48
50
  </playingEntity>
49
51
  </participantRole>
@@ -14,7 +14,7 @@
14
14
  <templateId root="2.16.840.1.113883.10.20.1.25"/>
15
15
  <!-- Plan of Activity activity template -->
16
16
  <id root="<%= UUID.generate %>"/>
17
- <%== code_display(entry) %>
17
+ <%== code_display(entry, :preferred_code_sets => ['SNOMED-CT']) %>
18
18
  <statusCode code="new"/>
19
19
  <effectiveTime value="<%= Time.at(entry.time).utc.to_formatted_s(:number) %>"/>
20
20
  </observation>
@@ -1,8 +1,13 @@
1
- <% if entry.single_code_value?
2
- code = entry.codes.first[1].first
3
- code_system_oid = QME::Importer::CodeSystemHelper.oid_for_code_system(entry.codes.first[0])
1
+ <% preferred_code = entry.preferred_code(preferred_code_sets)
2
+ if preferred_code
3
+ code_system_oid = HealthDataStandards::Util::CodeSystemHelper.oid_for_code_system(preferred_code['code_set'])
4
4
  -%>
5
- <code code="<%= code %>" codeSystem="<%= code_system_oid %>">
5
+ <code code="<%= preferred_code['code'] %>" codeSystem="<%= code_system_oid %>" displayName="<%= entry.description %>">
6
+ <% else -%>
7
+ <code nullFlavor="UNK">
8
+ <% end -%>
6
9
  <originalText><reference value="#<%= section %>-desc-<%= i %>"/></originalText>
10
+ <% entry.translation_codes(preferred_code_sets).each do |translation| -%>
11
+ <translation code="<%= translation['code'] %>" codeSystem="<%= HealthDataStandards::Util::CodeSystemHelper.oid_for_code_system(translation['code_set']) %>" />
12
+ <% end -%>
7
13
  </code>
8
- <% end -%>
@@ -7,7 +7,7 @@
7
7
  <!--Problems section template-->
8
8
  <code code="11450-4" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Problem list"/>
9
9
  <title>Problems</title>
10
- <%== render :partial => 'narrative_block', :locals => {:entries => entries, :section => 'conditions'} %>
10
+ <%== render :partial => 'narrative_block', :locals => {:entries => entries, :section => 'conditions', :status => true } %>
11
11
  <% entries.each_with_index do |entry, i| -%>
12
12
  <entry typeCode="DRIV">
13
13
  <act classCode="ACT" moodCode="EVN">
@@ -40,7 +40,7 @@
40
40
  <effectiveTime>
41
41
  <low value="<%= Time.at(entry.time).utc.to_formatted_s(:number) %>"/>
42
42
  </effectiveTime>
43
- <%== code_display(entry, 'value', 'xsi:type="CD"') %>
43
+ <%== code_display(entry, {'tag_name' => 'value', 'extra_content' => 'xsi:type="CD"', 'preferred_code_sets' => ['SNOMED-CT']}) %>
44
44
  <% if entry.status -%>
45
45
  <entryRelationship typeCode="REFR">
46
46
  <observation classCode="OBS" moodCode="EVN">
@@ -16,7 +16,8 @@
16
16
  <templateId root="1.3.6.1.4.1.19376.1.5.3.1.4.14" assigningAuthorityName="IHE PCC"/>
17
17
  <!-- Encounter activity template -->
18
18
  <id root="<%= UUID.generate %>"/>
19
- <%== render :partial => 'code_with_reference', :locals => {:entry => entry, :i => i, :section => 'encounters'} %>
19
+ <%== render :partial => 'code_with_reference', :locals => {:entry => entry, :i => i, :section => 'encounters',
20
+ :preferred_code_sets => ['CPT']} %>
20
21
  <text>
21
22
  <reference value="#encounters-desc-<%= i %>"/>
22
23
  </text>
@@ -29,7 +29,8 @@
29
29
  <templateId root="1.3.6.1.4.1.19376.1.5.3.1.4.7.2"/>
30
30
  <!-- Product template -->
31
31
  <manufacturedMaterial>
32
- <%== render :partial => 'code_with_reference', :locals => {:entry => entry, :i => i, :section => 'immunizations'} %>
32
+ <%== render :partial => 'code_with_reference', :locals => {:entry => entry, :i => i, :section => 'immunizations',
33
+ :preferred_code_sets => ['CVX']} %>
33
34
  <name><%= entry.description %></name>
34
35
  </manufacturedMaterial>
35
36
  </manufacturedProduct>
@@ -23,7 +23,7 @@
23
23
  <addr/>
24
24
  <telecom/>
25
25
  <playingDevice>
26
- <%== code_display(entry) %>
26
+ <%== code_display(entry, {:preferred_code_sets => ['SNOMED-CT']}) %>
27
27
  </playingDevice>
28
28
  </participantRole>
29
29
  </participant>
@@ -28,7 +28,8 @@
28
28
  <templateId root="2.16.840.1.113883.3.88.11.83.8.2"/>
29
29
  <templateId root="1.3.6.1.4.1.19376.1.5.3.1.4.7.2"/>
30
30
  <manufacturedMaterial>
31
- <%== render :partial => 'code_with_reference', :locals => {:entry => entry, :i => i, :section => 'medications'} %>
31
+ <%== render :partial => 'code_with_reference', :locals => {:entry => entry, :i => i, :section => 'medications',
32
+ :preferred_code_sets => ['RxNorm']} %>
32
33
  <name><%= entry.description %></name>
33
34
  </manufacturedMaterial>
34
35
  </manufacturedProduct>
@@ -5,6 +5,13 @@
5
5
  <th>Description</th>
6
6
  <th>Codes</th>
7
7
  <th>Time</th>
8
+ <% if status.present? %>
9
+ <th>Status</th>
10
+ <% end %>
11
+
12
+ <% if value.present? %>
13
+ <th>Value</th>
14
+ <% end %>
8
15
  </tr>
9
16
  </thead>
10
17
  <tbody>
@@ -13,6 +20,13 @@
13
20
  <td ID="<%= section %>-desc-<%= i %>"><%= entry.description %></td>
14
21
  <td ID="<%= section %>-code-<%= i %>"><%= entry.codes_to_s %></td>
15
22
  <td><%= entry.times_to_s %></td>
23
+ <% if status.present? %>
24
+ <td><%= entry.status %></td>
25
+ <% end %>
26
+
27
+ <% if value.present? %>
28
+ <td><%= entry.value["scalar"] %></td>
29
+ <% end %>
16
30
  </tr>
17
31
  <%- end -%>
18
32
  </tbody>