quality-measure-engine 0.1.2 → 0.2.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.
- data/Gemfile +9 -9
- data/README.md +39 -2
- data/Rakefile +25 -44
- data/js/map-reduce-utils.js +174 -0
- data/js/underscore-min.js +24 -0
- data/lib/qme/importer/code_system_helper.rb +26 -0
- data/lib/qme/importer/entry.rb +89 -0
- data/lib/qme/importer/generic_importer.rb +71 -0
- data/lib/qme/importer/hl7_helper.rb +27 -0
- data/lib/qme/importer/patient_importer.rb +150 -0
- data/lib/qme/importer/property_matcher.rb +103 -0
- data/lib/qme/importer/section_importer.rb +82 -0
- data/lib/qme/map/map_reduce_builder.rb +77 -147
- data/lib/qme/map/map_reduce_executor.rb +97 -13
- data/lib/qme/measure/database_loader.rb +90 -0
- data/lib/qme/measure/measure_loader.rb +141 -0
- data/lib/qme/mongo_helpers.rb +15 -0
- data/lib/qme/randomizer/patient_randomizer.rb +95 -0
- data/lib/qme_test.rb +13 -0
- data/lib/quality-measure-engine.rb +20 -4
- data/lib/tasks/measure.rake +76 -0
- data/lib/tasks/mongo.rake +74 -0
- data/lib/tasks/patient_random.rake +46 -0
- metadata +110 -156
- data/.gitignore +0 -6
- data/Gemfile.lock +0 -44
- data/fixtures/complex_measure.json +0 -36
- data/fixtures/result_example.json +0 -6
- data/lib/patches/v8.rb +0 -20
- data/lib/qme/query/json_document_builder.rb +0 -130
- data/lib/qme/query/json_query_executor.rb +0 -44
- data/measures/0032/0032_NQF_Cervical_Cancer_Screening.json +0 -171
- data/measures/0032/patients/denominator1.json +0 -10
- data/measures/0032/patients/denominator2.json +0 -10
- data/measures/0032/patients/numerator1.json +0 -11
- data/measures/0032/patients/population1.json +0 -9
- data/measures/0032/patients/population2.json +0 -11
- data/measures/0032/result/result.json +0 -6
- data/measures/0043/0043_NQF_PneumoniaVaccinationStatusForOlderAdults.json +0 -71
- data/measures/0043/patients/denominator.json +0 -11
- data/measures/0043/patients/numerator.json +0 -11
- data/measures/0043/patients/population.json +0 -10
- data/measures/0043/result/result.json +0 -6
- data/quality-measure-engine.gemspec +0 -97
- data/schema/result.json +0 -28
- data/schema/schema.json +0 -143
- data/spec/qme/map/map_reduce_builder_spec.rb +0 -64
- data/spec/qme/measures_spec.rb +0 -50
- data/spec/qme/query/json_document_builder_spec.rb +0 -56
- data/spec/schema_spec.rb +0 -21
- data/spec/spec_helper.rb +0 -7
- data/spec/validate_measures_spec.rb +0 -21
@@ -0,0 +1,71 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
# Class that can be used to create a HITSP C32 importer for any quality measure. This class will construct
|
4
|
+
# several SectionImporter for the various sections of the C32. When initialized with a JSON measure definition
|
5
|
+
# it can then be passed a C32 document and will return a Hash with all of the information needed to calculate the measure.
|
6
|
+
class GenericImporter
|
7
|
+
|
8
|
+
@@warnings = {}
|
9
|
+
|
10
|
+
# Creates a generic importer for any quality measure.
|
11
|
+
#
|
12
|
+
# @param [Hash] definition A measure definition described in JSON
|
13
|
+
def initialize(definition)
|
14
|
+
@definition = definition
|
15
|
+
end
|
16
|
+
|
17
|
+
# Parses a HITSP C32 document and returns a Hash of information related to the measure
|
18
|
+
#
|
19
|
+
# @param [Hash] patient_hash representation of a patient
|
20
|
+
# @return [Hash] measure information
|
21
|
+
def parse(patient_hash)
|
22
|
+
measure_info = {}
|
23
|
+
|
24
|
+
@definition['measure'].each_pair do |property, description|
|
25
|
+
raise "No standard_category for #{property}" if !description['standard_category']
|
26
|
+
matcher = PropertyMatcher.new(description)
|
27
|
+
entry_list = symbols_for_category(description['standard_category']).map do |section|
|
28
|
+
if patient_hash[section]
|
29
|
+
patient_hash[section]
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
end.flatten
|
34
|
+
if ! entry_list.empty?
|
35
|
+
matched_list = matcher.match(entry_list)
|
36
|
+
measure_info[property]=matched_list if matched_list.length>0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
measure_info
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def symbols_for_category(standard_category)
|
46
|
+
# Currently unsupported categories:
|
47
|
+
# characteristic, substance_allergy, medication_allergy, negation_rationale,
|
48
|
+
# diagnostic_study
|
49
|
+
case standard_category
|
50
|
+
when 'encounter'; [:encounters]
|
51
|
+
when 'procedure'; [:procedures]
|
52
|
+
when 'communication'; [:procedures]
|
53
|
+
when 'laboratory_test'; [:results, :vital_signs]
|
54
|
+
when 'physical_exam'; [:vital_signs]
|
55
|
+
when 'medication'; [:medications]
|
56
|
+
when 'diagnosis_condition_problem'; [:conditions, :social_history]
|
57
|
+
when 'characteristic'; [:conditions, :social_history]
|
58
|
+
when 'device'; [:conditions, :procedures, :care_goals, :medical_equipment]
|
59
|
+
when 'care_goal'; [:care_goals]
|
60
|
+
when 'diagnostic_study'; [:procedures]
|
61
|
+
else
|
62
|
+
if !@@warnings[standard_category]
|
63
|
+
puts "Warning: Unsupported standard_category (#{standard_category})"
|
64
|
+
@@warnings[standard_category]=true
|
65
|
+
end
|
66
|
+
[]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
|
4
|
+
# General helpers for working with HL7 data types
|
5
|
+
class HL7Helper
|
6
|
+
|
7
|
+
# Converts an HL7 timestamp into an Integer
|
8
|
+
# @param [String] timestamp the HL7 timestamp. Expects YYYYMMDD format
|
9
|
+
# @return [Integer] Date in seconds since the epoch
|
10
|
+
def self.timestamp_to_integer(timestamp)
|
11
|
+
if timestamp && timestamp.length >= 4
|
12
|
+
year = timestamp[0..3].to_i
|
13
|
+
month = timestamp.length >= 6 ? timestamp[4..5].to_i : 1
|
14
|
+
day = timestamp.length >= 8 ? timestamp[6..7].to_i : 1
|
15
|
+
hour = timestamp.length >= 10 ? timestamp[8..9].to_i : 0
|
16
|
+
min = timestamp.length >= 12 ? timestamp[10..11].to_i : 0
|
17
|
+
sec = timestamp.length >= 14 ? timestamp[12..13].to_i : 0
|
18
|
+
|
19
|
+
Time.gm(year, month, day, hour, min, sec).to_i
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
|
4
|
+
# This class is the central location for taking a HITSP C32 XML document and converting it
|
5
|
+
# into the processed form we store in MongoDB. The class does this by running each measure
|
6
|
+
# independently on the XML document
|
7
|
+
#
|
8
|
+
# This class is a Singleton. It should be accessed by calling PatientImporter.instance
|
9
|
+
class PatientImporter
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
# Creates a new PatientImporter with the following XPath expressions used to find content in
|
13
|
+
# a HITSP C32:
|
14
|
+
#
|
15
|
+
# Encounter entries
|
16
|
+
# //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.127']/cda:entry/cda:encounter
|
17
|
+
#
|
18
|
+
# Procedure entries
|
19
|
+
# //cda:procedure[cda:templateId/@root='2.16.840.1.113883.10.20.1.29']
|
20
|
+
#
|
21
|
+
# Result entries
|
22
|
+
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']
|
23
|
+
#
|
24
|
+
# Vital sign entries
|
25
|
+
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.14']
|
26
|
+
#
|
27
|
+
# Medication entries
|
28
|
+
# //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.112']/cda:entry/cda:substanceAdministration
|
29
|
+
#
|
30
|
+
# Codes for medications are found in the substanceAdministration with the following relative XPath
|
31
|
+
# ./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code
|
32
|
+
#
|
33
|
+
# Condition entries
|
34
|
+
# //cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.103']/cda:entry/cda:act/cda:entryRelationship/cda:observation
|
35
|
+
#
|
36
|
+
# Social History entries (non-C32 section, specified in the HL7 CCD)
|
37
|
+
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']
|
38
|
+
#
|
39
|
+
# Care Goal entries(non-C32 section, specified in the HL7 CCD)
|
40
|
+
# //cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']
|
41
|
+
#
|
42
|
+
# Codes for conditions are determined by examining the value child element as opposed to the code child element
|
43
|
+
def initialize
|
44
|
+
@measure_importers = {}
|
45
|
+
|
46
|
+
@section_importers = {}
|
47
|
+
@section_importers[:encounters] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.127']/cda:entry/cda:encounter")
|
48
|
+
@section_importers[:procedures] = SectionImporter.new("//cda:procedure[cda:templateId/@root='2.16.840.1.113883.10.20.1.29']")
|
49
|
+
@section_importers[:results] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.15']")
|
50
|
+
@section_importers[:vital_signs] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.14']")
|
51
|
+
@section_importers[:medications] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.112']/cda:entry/cda:substanceAdministration",
|
52
|
+
"./cda:consumable/cda:manufacturedProduct/cda:manufacturedMaterial/cda:code")
|
53
|
+
|
54
|
+
@section_importers[:conditions] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.103']/cda:entry/cda:act/cda:entryRelationship/cda:observation",
|
55
|
+
"./cda:value")
|
56
|
+
|
57
|
+
@section_importers[:social_history] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.19']")
|
58
|
+
@section_importers[:care_goals] = SectionImporter.new("//cda:observation[cda:templateId/@root='2.16.840.1.113883.10.20.1.25']")
|
59
|
+
@section_importers[:medical_equipment] = SectionImporter.new("//cda:section[cda:templateId/@root='2.16.840.1.113883.3.88.11.83.128']/cda:entry/cda:supply",
|
60
|
+
"./cda:participant/cda:participantRole/cda:playingDevice/cda:code")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parses a HITSP C32 document and returns a Hash of of the patient.
|
64
|
+
#
|
65
|
+
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
66
|
+
# will have the "cda" namespace registered to "urn:hl7-org:v3"
|
67
|
+
# @return [Hash] a representation of the patient that can be inserted into MongoDB
|
68
|
+
def parse_c32(doc)
|
69
|
+
patient_record = {}
|
70
|
+
c32_patient = create_c32_hash(doc)
|
71
|
+
get_demographics(patient_record, doc)
|
72
|
+
process_events(patient_record, c32_patient)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parses a patient hash containing demongraphic and event information
|
76
|
+
#
|
77
|
+
# @param [Hash] patient_hash patient data
|
78
|
+
# @return [Hash] a representation of the patient that can be inserted into MongoDB
|
79
|
+
def parse_hash(patient_hash)
|
80
|
+
patient_record = {}
|
81
|
+
patient_record['first'] = patient_hash['first']
|
82
|
+
patient_record['last'] = patient_hash['last']
|
83
|
+
patient_record['gender'] = patient_hash['gender']
|
84
|
+
patient_record['birthdate'] = patient_hash['birthdate']
|
85
|
+
event_hash = {}
|
86
|
+
patient_hash['events'].each do |key, value|
|
87
|
+
event_hash[key.intern] = parse_events(value)
|
88
|
+
end
|
89
|
+
process_events(patient_record, event_hash)
|
90
|
+
end
|
91
|
+
|
92
|
+
def process_events(patient_record, event_hash)
|
93
|
+
patient_record['measures'] = {}
|
94
|
+
@measure_importers.each_pair do |measure_id, importer|
|
95
|
+
patient_record['measures'][measure_id] = importer.parse(event_hash)
|
96
|
+
end
|
97
|
+
|
98
|
+
patient_record
|
99
|
+
end
|
100
|
+
|
101
|
+
# Parses a list of event hashes into an array of Entry objects
|
102
|
+
#
|
103
|
+
# @param [Array] event_list list of event hashes
|
104
|
+
# @return [Array] array of Entry objects
|
105
|
+
def parse_events(event_list)
|
106
|
+
event_list.collect do |event|
|
107
|
+
nil if event.class==String.class # skip
|
108
|
+
QME::Importer::Entry.from_event_hash(event)
|
109
|
+
end.compact
|
110
|
+
end
|
111
|
+
|
112
|
+
# Adds a measure to run on a C32 that is passed in
|
113
|
+
#
|
114
|
+
# @param [MeasureBase] measure an Class that can extract information from a C32 that is necessary
|
115
|
+
# to calculate the measure
|
116
|
+
def add_measure(measure_id, importer)
|
117
|
+
@measure_importers[measure_id] = importer
|
118
|
+
end
|
119
|
+
|
120
|
+
# Create a simple representation of the patient from a HITSP C32
|
121
|
+
#
|
122
|
+
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
123
|
+
# will have the "cda" namespace registered to "urn:hl7-org:v3"
|
124
|
+
# @return [Hash] a represnetation of the patient with symbols as keys for each section
|
125
|
+
def create_c32_hash(doc)
|
126
|
+
c32_patient = {}
|
127
|
+
@section_importers.each_pair do |section, importer|
|
128
|
+
c32_patient[section] = importer.create_entries(doc)
|
129
|
+
end
|
130
|
+
|
131
|
+
c32_patient
|
132
|
+
end
|
133
|
+
|
134
|
+
# Inspects a C32 document and populates the patient Hash with first name, last name
|
135
|
+
# birth date and gender.
|
136
|
+
#
|
137
|
+
# @param [Hash] patient A hash that is used to represent the patient
|
138
|
+
# @param [Nokogiri::XML::Node] doc The C32 document parsed by Nokogiri
|
139
|
+
def get_demographics(patient, doc)
|
140
|
+
patient['first'] = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:patient/cda:name/cda:given').text
|
141
|
+
patient['last'] = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:patient/cda:name/cda:family').text
|
142
|
+
birthdate_in_hl7ts_node = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:patient/cda:birthTime')
|
143
|
+
birthdate_in_hl7ts = birthdate_in_hl7ts_node['value']
|
144
|
+
patient['birthdate'] = HL7Helper.timestamp_to_integer(birthdate_in_hl7ts)
|
145
|
+
gender_node = doc.at_xpath('/cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:patient/cda:administrativeGenderCode')
|
146
|
+
patient['gender'] = gender_node['code']
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
# Compares Entry objects to measure definition properties.
|
4
|
+
class PropertyMatcher
|
5
|
+
|
6
|
+
def initialize(property_description)
|
7
|
+
@property_description = property_description
|
8
|
+
end
|
9
|
+
|
10
|
+
# Looks through an Array of Entry objects to see if any of them match the codes needed
|
11
|
+
# for a measure property. Will return different types of Arrays depending on the schema
|
12
|
+
# of the property
|
13
|
+
# @param [Array] entry_list an Array of Entry objects
|
14
|
+
# @return An Array of goodness that is ready to be inserted into a measure property on a patient record
|
15
|
+
def match(entry_list)
|
16
|
+
if is_date_list_property?
|
17
|
+
extract_date_list(entry_list)
|
18
|
+
elsif is_value_date_property?
|
19
|
+
extract_value_date_list(entry_list)
|
20
|
+
elsif is_date_range_property?
|
21
|
+
extract_date_range_list(entry_list)
|
22
|
+
else
|
23
|
+
raise "Unknown property schema for property #{@property_description['description']}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Extracts the dates of any CDA entries that meet the code set defined for measure property.
|
28
|
+
#
|
29
|
+
# @param [Array] entry_list an Array of Entry objects
|
30
|
+
# @return [Array] Provides an Array of dates for entries that have codes inside of the measure code set
|
31
|
+
# Dates will be represented as an Integer in seconds since the epoch
|
32
|
+
def extract_date_list(entry_list)
|
33
|
+
basic_extractor(entry_list) do |entry, matching_values|
|
34
|
+
matching_values << entry.as_point_in_time
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Extracts the dates of any CDA entries that meet the code set defined for measure property.
|
39
|
+
#
|
40
|
+
# @param [Array] entry_list an Array of Entry objects
|
41
|
+
# @return [Array] Provides an Array of Hashes for entries that have codes inside of the measure code set
|
42
|
+
# Hashes will have a "value" and "date" property containing the respective data
|
43
|
+
def extract_value_date_list(entry_list)
|
44
|
+
basic_extractor(entry_list) do |entry, matching_values|
|
45
|
+
if entry.value[:scalar]
|
46
|
+
matching_values << {'date' => entry.as_point_in_time, 'value' => entry.value[:scalar]}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Extracts the dates of any CDA entries that meet the code set defined for measure property.
|
52
|
+
#
|
53
|
+
# @param [Array] entry_list an Array of Entry objects
|
54
|
+
# @return [Array] Provides an Array of Hashes for entries that have codes inside of the measure code set
|
55
|
+
# Hashes will have a "start" and "end" property containing the respective data
|
56
|
+
def extract_date_range_list(entry_list)
|
57
|
+
basic_extractor(entry_list) do |entry, matching_values|
|
58
|
+
if entry.is_date_range?
|
59
|
+
matching_values << {'start' => entry.start_time, 'end' => entry.end_time}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Determines if the property is a list of dates
|
65
|
+
# @return [Boolean] true of false depending on the property
|
66
|
+
def is_date_list_property?
|
67
|
+
@property_description['type'] == 'array' && @property_description['items']['type'] == 'number'
|
68
|
+
end
|
69
|
+
|
70
|
+
# Determines if the property is a list of date and value hashes
|
71
|
+
# @return [Boolean] true of false depending on the property
|
72
|
+
def is_value_date_property?
|
73
|
+
@property_description['type'] == 'array' && @property_description['items']['type'] == 'object' &&
|
74
|
+
@property_description['items']['properties']['value'] &&
|
75
|
+
@property_description['items']['properties']['date']
|
76
|
+
end
|
77
|
+
|
78
|
+
# Determines if the property is a list of date ranges represented by a Hash with start and end
|
79
|
+
# keys
|
80
|
+
# @return [Boolean] true of false depending on the property
|
81
|
+
def is_date_range_property?
|
82
|
+
@property_description['type'] == 'array' && @property_description['items']['type'] == 'object' &&
|
83
|
+
@property_description['items']['properties']['start'] &&
|
84
|
+
@property_description['items']['properties']['end']
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def basic_extractor(entry_list)
|
90
|
+
matching_values = []
|
91
|
+
entry_list.each do |entry|
|
92
|
+
if entry.usable?
|
93
|
+
if entry.is_in_code_set?(@property_description['codes'])
|
94
|
+
yield entry, matching_values
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
matching_values
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module QME
|
2
|
+
module Importer
|
3
|
+
# Class that can be used to create an importer for a section of a HITSP C32 document. It usually
|
4
|
+
# operates by selecting all CDA entries in a section and then creates entries for them.
|
5
|
+
class SectionImporter
|
6
|
+
|
7
|
+
# Creates a new SectionImporter
|
8
|
+
# @param [String] entry_xpath An XPath expression that can be used to find the desired entries
|
9
|
+
# @param [String] code_xpath XPath expression to find the code element as a child of the desired CDA entry.
|
10
|
+
# Defaults to "./cda:code"
|
11
|
+
def initialize(entry_xpath, code_xpath="./cda:code")
|
12
|
+
@entry_xpath = entry_xpath
|
13
|
+
@code_xpath = code_xpath
|
14
|
+
end
|
15
|
+
|
16
|
+
# Traverses that HITSP C32 document passed in using XPath and creates an Array of Entry
|
17
|
+
# objects based on what it finds
|
18
|
+
# @param [Nokogiri::XML::Document] doc It is expected that the root node of this document
|
19
|
+
# will have the "cda" namespace registered to "urn:hl7-org:v3"
|
20
|
+
# measure definition
|
21
|
+
# @return [Array] will be a list of Entry objects
|
22
|
+
def create_entries(doc)
|
23
|
+
entry_list = []
|
24
|
+
entry_elements = doc.xpath(@entry_xpath)
|
25
|
+
entry_elements.each do |entry_element|
|
26
|
+
entry = Entry.new
|
27
|
+
extract_codes(entry_element, entry)
|
28
|
+
extract_dates(entry_element, entry)
|
29
|
+
extract_value(entry_element, entry)
|
30
|
+
if entry.usable?
|
31
|
+
entry_list << entry
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
entry_list
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def extract_codes(parent_element, entry)
|
41
|
+
code_elements = parent_element.xpath(@code_xpath)
|
42
|
+
code_elements.each do |code_element|
|
43
|
+
add_code_if_present(code_element, entry)
|
44
|
+
|
45
|
+
translations = code_element.xpath('cda:translation')
|
46
|
+
translations.each do |translation|
|
47
|
+
add_code_if_present(translation, entry)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_code_if_present(code_element, entry)
|
53
|
+
if code_element['codeSystem'] && code_element['code']
|
54
|
+
entry.add_code(code_element['code'], CodeSystemHelper.code_system_for(code_element['codeSystem']))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_dates(parent_element, entry)
|
59
|
+
if parent_element.at_xpath('cda:effectiveTime')
|
60
|
+
entry.time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime')['value'])
|
61
|
+
end
|
62
|
+
if parent_element.at_xpath('cda:effectiveTime/cda:low')
|
63
|
+
entry.start_time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime/cda:low')['value'])
|
64
|
+
end
|
65
|
+
if parent_element.at_xpath('cda:effectiveTime/cda:high')
|
66
|
+
entry.end_time = HL7Helper.timestamp_to_integer(parent_element.at_xpath('cda:effectiveTime/cda:high')['value'])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def extract_value(parent_element, entry)
|
71
|
+
value_element = parent_element.at_xpath('cda:value')
|
72
|
+
if value_element
|
73
|
+
value = value_element['value']
|
74
|
+
unit = value_element['unit']
|
75
|
+
if value
|
76
|
+
entry.set_value(value, unit)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -1,169 +1,99 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'ostruct'
|
3
|
+
|
1
4
|
module QME
|
2
5
|
module MapReduce
|
6
|
+
|
7
|
+
# Builds Map and Reduce functions for a particular measure
|
3
8
|
class Builder
|
4
|
-
attr_reader :id, :
|
5
|
-
|
6
|
-
YEAR_IN_SECONDS = 365*24*60*60
|
9
|
+
attr_reader :id, :params
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
raise "No value supplied for measure parameter: #{parameter}"
|
16
|
-
end
|
17
|
-
@parameters[parameter.intern] = params[parameter.intern]
|
18
|
-
end
|
19
|
-
ctx = V8::Context.new
|
20
|
-
ctx['year']=YEAR_IN_SECONDS
|
21
|
-
@parameters.each do |key, param|
|
22
|
-
ctx[key]=param
|
11
|
+
# Utility class used to supply a binding to Erb
|
12
|
+
class Context < OpenStruct
|
13
|
+
# Create a new context
|
14
|
+
# @param [Hash] vars a hash of parameter names (String) and values (Object). Each entry is added as an accessor of the new Context
|
15
|
+
def initialize(db, vars)
|
16
|
+
super(vars)
|
17
|
+
@db = db
|
23
18
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
19
|
+
|
20
|
+
# Get a binding that contains all the instance variables
|
21
|
+
# @return [Binding]
|
22
|
+
def get_binding
|
23
|
+
binding
|
27
24
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
REDUCE_FUNCTION = <<END_OF_REDUCE_FN
|
51
|
-
function (key, values) {
|
52
|
-
var total = {i: 0, d: 0, n: 0, e: 0};
|
53
|
-
for (var i = 0; i < values.length; i++) {
|
54
|
-
total.i += values[i].i;
|
55
|
-
total.d += values[i].d;
|
56
|
-
total.n += values[i].n;
|
57
|
-
total.e += values[i].e;
|
58
|
-
}
|
59
|
-
return total;
|
60
|
-
};
|
61
|
-
END_OF_REDUCE_FN
|
62
|
-
|
63
|
-
def reduce_function
|
64
|
-
REDUCE_FUNCTION
|
65
|
-
end
|
66
|
-
|
67
|
-
def population
|
68
|
-
javascript(@measure_def['population'])
|
69
|
-
end
|
70
|
-
|
71
|
-
def denominator
|
72
|
-
javascript(@measure_def['denominator'])
|
73
|
-
end
|
74
|
-
|
75
|
-
def numerator
|
76
|
-
javascript(@measure_def['numerator'])
|
77
|
-
end
|
78
|
-
|
79
|
-
def exception
|
80
|
-
javascript(@measure_def['exception'])
|
81
|
-
end
|
82
|
-
|
83
|
-
def javascript(expr)
|
84
|
-
if expr.has_key?('query')
|
85
|
-
# leaf node
|
86
|
-
query = expr['query']
|
87
|
-
triple = leaf_expr(query)
|
88
|
-
property_name = munge_property_name(triple[0])
|
89
|
-
'('+property_name+triple[1]+triple[2]+')'
|
90
|
-
elsif expr.size==1
|
91
|
-
operator = expr.keys[0]
|
92
|
-
result = logical_expr(operator, expr[operator])
|
93
|
-
operator = result.shift
|
94
|
-
js = '('
|
95
|
-
result.each_with_index do |operand,index|
|
96
|
-
if index>0
|
97
|
-
js+=operator
|
25
|
+
|
26
|
+
# Inserts any library code into the measure JS. JS library code is loaded from
|
27
|
+
# three locations: the js directory of the quality-measure-engine project, the
|
28
|
+
# js sub-directory of the current directory (e.g. measures/js), and the bundles
|
29
|
+
# collection of the current database (used by the Rails Web application).
|
30
|
+
def init_js_frameworks
|
31
|
+
result = ''
|
32
|
+
result << 'if (typeof(map)=="undefined") {'
|
33
|
+
result << "\n"
|
34
|
+
Dir.glob(File.join(File.dirname(__FILE__), '../../../js/*.js')).each do |js_file|
|
35
|
+
result << File.read(js_file)
|
36
|
+
result << "\n"
|
37
|
+
end
|
38
|
+
Dir.glob(File.join('./js/*.js')).each do |js_file|
|
39
|
+
result << File.read(js_file)
|
40
|
+
result << "\n"
|
41
|
+
end
|
42
|
+
@db['bundles'].find.each do |bundle|
|
43
|
+
(bundle['extensions'] || []).each do |ext|
|
44
|
+
result << ext
|
45
|
+
result << "\n"
|
98
46
|
end
|
99
|
-
js+=operand
|
100
47
|
end
|
101
|
-
|
102
|
-
|
103
|
-
elsif expr.size==0
|
104
|
-
'(false)'
|
105
|
-
else
|
106
|
-
throw "Unexpected number of keys in: #{expr}"
|
48
|
+
result << "}\n"
|
49
|
+
result
|
107
50
|
end
|
108
51
|
end
|
109
52
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
53
|
+
# Create a new Builder
|
54
|
+
# @param [Hash] measure_def a JSON hash of the measure, field values may contain Erb directives to inject the values of supplied parameters into the map function
|
55
|
+
# @param [Hash] params a hash of parameter names (String or Symbol) and their values
|
56
|
+
def initialize(db, measure_def, params)
|
57
|
+
@id = measure_def['id']
|
58
|
+
@params = {}
|
59
|
+
@db = db
|
60
|
+
|
61
|
+
# normalize parameters hash to accept either symbol or string keys
|
62
|
+
params.each do |name, value|
|
63
|
+
@params[name.to_s] = value
|
64
|
+
end
|
65
|
+
@measure_def = measure_def
|
66
|
+
@measure_def['parameters'] ||= {}
|
67
|
+
@measure_def['parameters'].each do |parameter, value|
|
68
|
+
if !@params.has_key?(parameter)
|
69
|
+
raise "No value supplied for measure parameter: #{parameter}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# if the map function is specified then replace any erb templates with their values
|
73
|
+
# taken from the supplied params
|
74
|
+
# always true for actual measures, not always true for unit tests
|
75
|
+
if (@measure_def['map_fn'])
|
76
|
+
template = ERB.new(@measure_def['map_fn'])
|
77
|
+
context = Context.new(@db, @params)
|
78
|
+
@measure_def['map_fn'] = template.result(context.get_binding)
|
115
79
|
end
|
116
80
|
end
|
117
81
|
|
118
|
-
|
119
|
-
|
120
|
-
|
82
|
+
# Get the map function for the measure
|
83
|
+
# @return [String] the map function
|
84
|
+
def map_function
|
85
|
+
@measure_def['map_fn']
|
121
86
|
end
|
122
87
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
[property_name, get_operator(operator), get_value(value)]
|
130
|
-
else
|
131
|
-
[property_name, '==', get_value(property_value_expression)]
|
132
|
-
end
|
88
|
+
# Get the reduce function for the measure, this is a simple
|
89
|
+
# wrapper for the reduce utility function specified in
|
90
|
+
# map-reduce-utils.js
|
91
|
+
# @return [String] the reduce function
|
92
|
+
def reduce_function
|
93
|
+
'function (key, values) { return reduce(key, values);};'
|
133
94
|
end
|
134
95
|
|
135
|
-
def get_operator(operator)
|
136
|
-
case operator
|
137
|
-
when '_eql'
|
138
|
-
'=='
|
139
|
-
when '_gt'
|
140
|
-
'>'
|
141
|
-
when '_gte'
|
142
|
-
'>='
|
143
|
-
when '_lt'
|
144
|
-
'<'
|
145
|
-
when '_lte'
|
146
|
-
'<='
|
147
|
-
when 'and'
|
148
|
-
'&&'
|
149
|
-
when 'or'
|
150
|
-
'||'
|
151
|
-
else
|
152
|
-
throw "Unknown operator: #{operator}"
|
153
|
-
end
|
154
|
-
end
|
155
96
|
|
156
|
-
def get_value(value)
|
157
|
-
if value.kind_of?(String) && value[0]=='@'
|
158
|
-
@parameters[value[1..-1].intern].to_s
|
159
|
-
elsif value.kind_of?(String)
|
160
|
-
'"'+value+'"'
|
161
|
-
elsif value==nil
|
162
|
-
'null'
|
163
|
-
else
|
164
|
-
value.to_s
|
165
|
-
end
|
166
|
-
end
|
167
97
|
end
|
168
98
|
end
|
169
99
|
end
|