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.
Files changed (52) hide show
  1. data/Gemfile +9 -9
  2. data/README.md +39 -2
  3. data/Rakefile +25 -44
  4. data/js/map-reduce-utils.js +174 -0
  5. data/js/underscore-min.js +24 -0
  6. data/lib/qme/importer/code_system_helper.rb +26 -0
  7. data/lib/qme/importer/entry.rb +89 -0
  8. data/lib/qme/importer/generic_importer.rb +71 -0
  9. data/lib/qme/importer/hl7_helper.rb +27 -0
  10. data/lib/qme/importer/patient_importer.rb +150 -0
  11. data/lib/qme/importer/property_matcher.rb +103 -0
  12. data/lib/qme/importer/section_importer.rb +82 -0
  13. data/lib/qme/map/map_reduce_builder.rb +77 -147
  14. data/lib/qme/map/map_reduce_executor.rb +97 -13
  15. data/lib/qme/measure/database_loader.rb +90 -0
  16. data/lib/qme/measure/measure_loader.rb +141 -0
  17. data/lib/qme/mongo_helpers.rb +15 -0
  18. data/lib/qme/randomizer/patient_randomizer.rb +95 -0
  19. data/lib/qme_test.rb +13 -0
  20. data/lib/quality-measure-engine.rb +20 -4
  21. data/lib/tasks/measure.rake +76 -0
  22. data/lib/tasks/mongo.rake +74 -0
  23. data/lib/tasks/patient_random.rake +46 -0
  24. metadata +110 -156
  25. data/.gitignore +0 -6
  26. data/Gemfile.lock +0 -44
  27. data/fixtures/complex_measure.json +0 -36
  28. data/fixtures/result_example.json +0 -6
  29. data/lib/patches/v8.rb +0 -20
  30. data/lib/qme/query/json_document_builder.rb +0 -130
  31. data/lib/qme/query/json_query_executor.rb +0 -44
  32. data/measures/0032/0032_NQF_Cervical_Cancer_Screening.json +0 -171
  33. data/measures/0032/patients/denominator1.json +0 -10
  34. data/measures/0032/patients/denominator2.json +0 -10
  35. data/measures/0032/patients/numerator1.json +0 -11
  36. data/measures/0032/patients/population1.json +0 -9
  37. data/measures/0032/patients/population2.json +0 -11
  38. data/measures/0032/result/result.json +0 -6
  39. data/measures/0043/0043_NQF_PneumoniaVaccinationStatusForOlderAdults.json +0 -71
  40. data/measures/0043/patients/denominator.json +0 -11
  41. data/measures/0043/patients/numerator.json +0 -11
  42. data/measures/0043/patients/population.json +0 -10
  43. data/measures/0043/result/result.json +0 -6
  44. data/quality-measure-engine.gemspec +0 -97
  45. data/schema/result.json +0 -28
  46. data/schema/schema.json +0 -143
  47. data/spec/qme/map/map_reduce_builder_spec.rb +0 -64
  48. data/spec/qme/measures_spec.rb +0 -50
  49. data/spec/qme/query/json_document_builder_spec.rb +0 -56
  50. data/spec/schema_spec.rb +0 -21
  51. data/spec/spec_helper.rb +0 -7
  52. 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, :parameters
5
-
6
- YEAR_IN_SECONDS = 365*24*60*60
9
+ attr_reader :id, :params
7
10
 
8
- def initialize(measure_def, params)
9
- @measure_def = measure_def
10
- @id = measure_def['id']
11
- @parameters = {}
12
- measure_def['parameters'] ||= {}
13
- measure_def['parameters'].each do |parameter, value|
14
- if !params.has_key?(parameter.intern)
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
- measure_def['calculated_dates'] ||= {}
25
- measure_def['calculated_dates'].each do |parameter, value|
26
- @parameters[parameter.intern]=ctx.eval(value)
19
+
20
+ # Get a binding that contains all the instance variables
21
+ # @return [Binding]
22
+ def get_binding
23
+ binding
27
24
  end
28
- @property_prefix = 'this.measures["'+@id+'"].'
29
- end
30
-
31
- def map_function
32
- "function () {\n" +
33
- " var value = {i: 0, d: 0, n: 0, e: 0};\n" +
34
- " if #{population} {\n" +
35
- " value.i++;\n" +
36
- " if #{denominator} {\n" +
37
- " value.d++;\n" +
38
- " if #{numerator} {\n" +
39
- " value.n++;\n" +
40
- " } else if #{exception} {\n" +
41
- " value.e++;\n" +
42
- " value.d--;\n" +
43
- " }\n" +
44
- " }\n" +
45
- " }\n" +
46
- " emit(null, value);\n" +
47
- "};\n"
48
- end
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
- js+=')'
102
- js
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
- def munge_property_name(name)
111
- if name=='birthdate'
112
- 'this.'+name
113
- else
114
- @property_prefix+name
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
- def logical_expr(operator, args)
119
- operands = args.collect { |arg| javascript(arg) }
120
- [get_operator(operator)].concat(operands)
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
- def leaf_expr(query)
124
- property_name = query.keys[0]
125
- property_value_expression = query[property_name]
126
- if property_value_expression.kind_of?(Hash)
127
- operator = property_value_expression.keys[0]
128
- value = property_value_expression[operator]
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