quality-measure-engine 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|