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.
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