hqmf2js 1.3.0 → 1.4.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 (40) hide show
  1. checksums.yaml +5 -13
  2. data/.travis.yml +1 -1
  3. data/Gemfile +1 -25
  4. data/Gemfile.lock +170 -146
  5. data/app/assets/javascripts/crosswalk.js.coffee +17 -19
  6. data/app/assets/javascripts/custom_calculations.js.coffee +44 -17
  7. data/app/assets/javascripts/hqmf_util.js.coffee +559 -161
  8. data/app/assets/javascripts/logging_utils.js.coffee +6 -4
  9. data/app/assets/javascripts/patient_api_extension.js.coffee +41 -9
  10. data/app/assets/javascripts/specifics.js.coffee +163 -69
  11. data/hqmf2js.gemspec +7 -12
  12. data/lib/assets/javascripts/libraries/map_reduce_utils.js +151 -64
  13. data/lib/generator/characteristic.js.erb +23 -12
  14. data/lib/generator/codes_to_json.rb +1 -1
  15. data/lib/generator/data_criteria.js.erb +15 -3
  16. data/lib/generator/derived_data.js.erb +5 -0
  17. data/lib/generator/execution.rb +41 -11
  18. data/lib/generator/js.rb +74 -41
  19. data/lib/generator/patient_data.js.erb +1 -1
  20. data/lib/hqmf2js.rb +0 -1
  21. data/lib/hquery/engine.rb +3 -1
  22. data/lib/tasks/convert.rake +20 -12
  23. data/test/fixtures/NQF59New.json +1423 -0
  24. data/test/fixtures/fulfills.xml +917 -0
  25. data/test/fixtures/patients/larry_vanderman.json +573 -654
  26. data/test/{simplecov.rb → simplecov_init.rb} +0 -0
  27. data/test/test_helper.rb +2 -3
  28. data/test/unit/cmd_test.rb +145 -19
  29. data/test/unit/codes_to_json_test.rb +12 -12
  30. data/test/unit/custom_calculations_test.rb +2 -6
  31. data/test/unit/effective_date_test.rb +3 -4
  32. data/test/unit/erb_context_test.rb +12 -12
  33. data/test/unit/filter_by_reference_test.rb +39 -0
  34. data/test/unit/hqmf_from_json_javascript_test.rb +2 -1
  35. data/test/unit/hqmf_javascript_test.rb +12 -13
  36. data/test/unit/js_object_test.rb +2 -2
  37. data/test/unit/library_function_test.rb +210 -42
  38. data/test/unit/specifics_test.rb +402 -321
  39. metadata +57 -15
  40. data/config/warble.rb +0 -144
@@ -9,7 +9,9 @@ class @Logger
9
9
  if @enable_rationale and result? and typeof(result.isTrue) == 'function'
10
10
  if result.isTrue() and result.length
11
11
  json_results = _.map(result,(item) -> {id: item.id, json: item.json})
12
- @rationale[id] = {results: json_results }
12
+ if result.specificContext?
13
+ specific_ids = result.specificContext.flattenToIds()
14
+ @rationale[id] = {results: json_results, specifics: specific_ids }
13
15
  else
14
16
  @rationale[id] = result.isTrue()
15
17
 
@@ -132,9 +134,9 @@ class @Logger
132
134
  );
133
135
 
134
136
  # Wrap selected HQMF Util functions
135
- hqmf.SpecificsManagerSingleton.prototype.intersectAll = _.wrap(hqmf.SpecificsManagerSingleton.prototype.intersectAll, (func, boolVal, values, negate=false, episodeIndices) ->
136
- func = _.bind(func, this, boolVal, values, negate, episodeIndices)
137
- result = func(boolVal, values, negate, episodeIndices)
137
+ hqmf.SpecificsManagerSingleton.prototype.intersectAll = _.wrap(hqmf.SpecificsManagerSingleton.prototype.intersectAll, (func, boolVal, values, negate=false, episodeIndices, options) ->
138
+ func = _.bind(func, this, boolVal, values, negate, episodeIndices, options)
139
+ result = func(boolVal, values, negate, episodeIndices, options)
138
140
  Logger.info("Intersecting (#{values.length}):")
139
141
  for value in values
140
142
  Logger.logSpecificContext(value)
@@ -54,6 +54,17 @@ hQuery.Encounter::lengthOfStay = (unit) ->
54
54
  ivl_ts = this.asIVL_TS()
55
55
  ivl_ts.low.difference(ivl_ts.high, unit)
56
56
 
57
+ hQuery.Encounter::transferTime = () ->
58
+ transfer = (@json['transferFrom'] || @json['transferTo'])
59
+ time = transfer.time if transfer
60
+ if time
61
+ hQuery.dateFromUtcSeconds(time)
62
+ else
63
+ if @json['transferTo']
64
+ @endDate()
65
+ else
66
+ @startDate()
67
+
57
68
  hQuery.AdministrationTiming::dosesPerDay = () ->
58
69
  #figure out the units and value and calculate
59
70
  p = this.period()
@@ -64,9 +75,9 @@ hQuery.AdministrationTiming::dosesPerDay = () ->
64
75
  1/p.value()
65
76
 
66
77
 
67
- hQuery.Fulfillment::daysInRange = (dateRange,doesPerDay) ->
78
+ hQuery.Fulfillment::daysInRange = (dateRange,dose, dosesPerDay) ->
68
79
  # this will give us the number of days this fullfilment was for
69
- totalDays = this.quantityDispensed().value()/doesPerDay
80
+ totalDays = this.quantityDispensed().value()/dose/dosesPerDay
70
81
  totalDays = 0 if isNaN(totalDays)
71
82
  endDate = new Date(this.dispenseDate().getTime() + (totalDays*60*60*24*1000))
72
83
  high = if dateRange && dateRange.high then dateRange.high.asDate() else endDate
@@ -94,18 +105,39 @@ hQuery.Fulfillment::daysInRange = (dateRange,doesPerDay) ->
94
105
  # date range
95
106
  hQuery.Medication::fulfillmentTotals = (dateRange)->
96
107
  dpd = this.administrationTiming().dosesPerDay()
108
+ dose = this.dose().scalar
97
109
  this.fulfillmentHistory().reduce (t, s) ->
98
- t + s.daysInRange(dateRange,dpd)
110
+ t + s.daysInRange(dateRange,dose,dpd)
99
111
  , 0
100
-
112
+
113
+ # returns cumulativeMedicationDuration in terms of days
101
114
  hQuery.Medication::cumulativeMedicationDuration = (dateRange) ->
102
- #assuming that the dose is the same across fills and that fills is stated in individual
103
- #doses not total amount. Will need to flush this out more at a later point in time.
104
- #Considering that liquid meds are probaly dispensed as total volume ex 325ml with a dose of
105
- #say 25ml per dose. Will definatley need to revisit this.
106
- this.fulfillmentTotals(dateRange) if this.administrationTiming()
115
+ #assuming that the dose is the same across fills and that fills is stated in individual
116
+ #doses not total amount. Will need to flush this out more at a later point in time.
117
+ #Considering that liquid meds are probaly dispensed as total volume ex 325ml with a dose of
118
+ #say 25ml per dose. Will definatley need to revisit this.
119
+ if this.administrationTiming() && this.dose() && @json['fulfillmentHistory']
120
+ this.fulfillmentTotals(dateRange)
121
+ else if this.administrationTiming() && this.allowedAdministrations()
122
+ # this happens if we have a Medication, Order.
123
+ cumulativeMedicationDuration = this.allowedAdministrations() / this.administrationTiming().dosesPerDay()
124
+ # need to do in case of divide by zero error
125
+ cumulativeMedicationDuration = 0 if isNaN(cumulativeMedicationDuration)
126
+ cumulativeMedicationDuration
127
+
128
+ class hQuery.Reference
129
+ constructor: (@json) ->
130
+ referenced_id: -> @json["referenced_id"]
131
+ referenced_type: -> @json["reference"]
132
+ type: -> @json["type"]
133
+
107
134
 
135
+ hQuery.CodedEntry::references = () ->
136
+ for ref in (@json["references"] || [])
137
+ new hQuery.Reference(ref)
108
138
 
139
+ hQuery.CodedEntry::referencesByType = (type) ->
140
+ e for e in @references() when e.type() == type
109
141
 
110
142
  hQuery.CodedEntry::respondTo = (functionName) ->
111
143
  typeof(@[functionName]) == "function"
@@ -27,9 +27,22 @@ class hqmf.SpecificsManagerSingleton
27
27
  @keyLookup[i] = occurrenceKey.id
28
28
  @indexLookup[occurrenceKey.id] = i
29
29
  @functionLookup[i] = occurrenceKey.function
30
- @typeLookup[occurrenceKey.type] ||= []
31
- @typeLookup[occurrenceKey.type].push(i)
32
-
30
+ # LDY 8/25/17
31
+ # Something changed in the MAT so the "type" is no longer included in the HQMF. The backup
32
+ # type included too much detail (OccurrenceA_... and OccurrenceB_...), which made the types
33
+ # appear different for two occurrences of the same type.
34
+ # The code below ignores "Occ..." strings within the "type". This makes it so the type will
35
+ # now appear the same where appropriate.
36
+ # Note: OccurrenceA... is used for regular instances of an occurrence. OccA... is used for
37
+ # QDM variables.
38
+ generic_type = occurrenceKey.type
39
+ match = generic_type.match(/^occ[a-z]*_(.*)/i)
40
+ if match
41
+ generic_type = match[1]
42
+ if generic_type not of @typeLookup
43
+ @typeLookup[generic_type] = []
44
+ @typeLookup[generic_type].push(i)
45
+
33
46
  _generateCartisian: (allValues) ->
34
47
  _.reduce(allValues, (as, bs) ->
35
48
  product = []
@@ -42,41 +55,34 @@ class hqmf.SpecificsManagerSingleton
42
55
  identity: ->
43
56
  new hqmf.SpecificOccurrence([new Row(undefined)])
44
57
 
45
- setIfNull: (events,subsets) ->
46
- if (!events.specificContext? || events.length == 0)
47
- events.specificContext=hqmf.SpecificsManager.identity()
58
+ setIfNull: (events) ->
59
+ # Add specifics if missing, appropriately based on the truthiness
60
+ if !events.specificContext?
61
+ if events.isTrue()
62
+ events.specificContext=hqmf.SpecificsManager.identity()
63
+ else
64
+ events.specificContext=hqmf.SpecificsManager.empty()
65
+ events
48
66
 
49
67
  getColumnIndex: (occurrenceID) ->
50
68
  columnIndex = @indexLookup[occurrenceID]
51
69
  if typeof columnIndex == "undefined"
52
- throw "Unknown occurrence identifier: "+occurrenceID
70
+ throw new Error("Unknown occurrence identifier: "+occurrenceID)
53
71
  columnIndex
54
72
 
55
73
  empty: ->
56
74
  new hqmf.SpecificOccurrence([])
57
75
 
76
+ # Extract events for leftmost of supplied rows, returning copies with a specificRow attribute set
58
77
  extractEventsForLeftMost: (rows) ->
59
78
  events = []
60
79
  for row in rows
61
- events.push(@extractEvent(row.leftMost, row)) if row.leftMost? || row.tempValue?
80
+ for event in row.leftMostEvents()
81
+ event = new event.constructor(event.json)
82
+ event.specificRow = row
83
+ events.push(event)
62
84
  events
63
-
64
- extractEvents: (key, rows) ->
65
- events = []
66
- for row in rows
67
- events.push(@extractEvent(key, row))
68
- events
69
-
70
- extractEvent: (key, row) ->
71
- index = @indexLookup[key]
72
- if index?
73
- entry = row.values[index]
74
- else
75
- entry = row.tempValue
76
- entry = new hQuery.CodedEntry(entry.json)
77
- entry.specificRow = row
78
- entry
79
-
85
+
80
86
  intersectSpecifics: (nextPopulation, previousPopulation, occurrenceIDs) ->
81
87
  # we need to pass the episode indicies all the way down through the interesection to the match function
82
88
  # this must be done because we need to ensure that on intersection of populations the * does not allow an episode through
@@ -119,13 +125,13 @@ class hqmf.SpecificsManagerSingleton
119
125
  validate: (intersectedPopulation) ->
120
126
  intersectedPopulation.isTrue() and intersectedPopulation.specificContext.hasRows()
121
127
 
122
- intersectAll: (boolVal, values, negate=false, episodeIndices) ->
128
+ intersectAll: (boolVal, values, negate=false, episodeIndices, options = {}) ->
123
129
  result = new hqmf.SpecificOccurrence
124
130
  # add identity row
125
131
  result.addIdentityRow()
126
132
  for value in values
127
133
  if value.specificContext?
128
- result = result.intersect(value.specificContext, episodeIndices)
134
+ result = result.intersect(value.specificContext, episodeIndices, options)
129
135
  if negate and (!result.hasRows() or result.hasSpecifics())
130
136
  result = result.negate()
131
137
  result = result.compactReusedEvents()
@@ -153,10 +159,33 @@ class hqmf.SpecificsManagerSingleton
153
159
  boolVal = new Boolean(true) if @occurrences.length > 0
154
160
  boolVal.specificContext = result
155
161
  boolVal
156
-
162
+
163
+ # Given a set of events with a specificContext, filter the events to include only those
164
+ # referenced in the specific context
165
+ filterEventsAgainstSpecifics: (events) ->
166
+ # If there are no specifics (ie identity) we return them all as-is
167
+ return events unless events.specificContext.hasSpecifics()
168
+
169
+ # Find all the events referenced in the specific context
170
+ referencedEvents = hqmf.SpecificsManager.extractEventsForLeftMost(events.specificContext.rows)
171
+ referencedEventIds = _(referencedEvents).pluck('id')
172
+
173
+ # Filter original events to only return referenced ones (and ones without an ID, likely dates)
174
+ result = _(events).select (e) -> !e.id || _(referencedEventIds).contains(e.id)
175
+
176
+ # Copy the specifics over and return the result
177
+ hqmf.SpecificsManager.maintainSpecifics(result, events)
178
+ return result
179
+
157
180
  # copy the specifics parameters from an existing element onto the new value element
158
181
  maintainSpecifics: (newElement, existingElement) ->
159
- newElement.specificContext = existingElement.specificContext
182
+ # We handle a special case: if the existing element is falsy (ie an empty result set), and the new element
183
+ # is truthy (ie a boolean true), and the specific context is the empty set (no rows), we change it to the
184
+ # identity; this can happen, for example, if the new element is checking COUNT=0 of the existing element
185
+ if newElement.isTrue() && existingElement.isFalse() && existingElement.specificContext? && !existingElement.specificContext.hasRows()
186
+ newElement.specificContext = hqmf.SpecificsManager.identity()
187
+ else
188
+ newElement.specificContext = existingElement.specificContext
160
189
  newElement.specific_occurrence = existingElement.specific_occurrence
161
190
  newElement
162
191
 
@@ -194,11 +223,10 @@ class hqmf.SpecificOccurrence
194
223
  result
195
224
 
196
225
  removeDuplicateRows: () ->
197
- deduped = new hqmf.SpecificOccurrence
198
- for row in @rows
199
- # this could potentially be hasRow to dump even more rows.
200
- deduped.addRows([row]) if !deduped.hasExactRow(row)
201
- deduped
226
+ # Uniq rows based on each row's string transformation
227
+ uniqRows = {}
228
+ uniqRows[row.toHashKey()] = row for row in @rows
229
+ new hqmf.SpecificOccurrence(_(uniqRows).values())
202
230
 
203
231
  # Returns a count of unique events for a supplied column index
204
232
  uniqueEvents: (columnIndices) ->
@@ -229,21 +257,21 @@ class hqmf.SpecificOccurrence
229
257
  value.rows = @rows.concat(other.rows)
230
258
  value.removeDuplicateRows()
231
259
 
232
- intersect: (other, episodeIndices) ->
260
+ intersect: (other, episodeIndices, options = {}) ->
233
261
  value = new hqmf.SpecificOccurrence()
234
262
  for leftRow in @rows
235
263
  for rightRow in other.rows
236
- result = leftRow.intersect(rightRow, episodeIndices)
264
+ result = leftRow.intersect(rightRow, episodeIndices, options)
237
265
  value.rows.push(result) if result?
238
266
  value.removeDuplicateRows()
239
267
 
240
268
  getLeftMost: ->
241
- leftMost = undefined
269
+ specificLeftMost = undefined
242
270
  for row in @rows
243
- leftMost = row.leftMost unless leftMost?
244
- return undefined if leftMost != row.leftMost
245
- leftMost
246
-
271
+ specificLeftMost = row.specificLeftMost unless specificLeftMost?
272
+ return undefined if specificLeftMost != row.specificLeftMost
273
+ specificLeftMost
274
+
247
275
  negate: ->
248
276
  negatedRows = []
249
277
  keys = []
@@ -273,6 +301,22 @@ class hqmf.SpecificOccurrence
273
301
  newRows.push(myRow) if goodRow
274
302
  new hqmf.SpecificOccurrence(newRows)
275
303
 
304
+ # Given a set of events, return new specifics removing any rows that *do not* refer to that set of events
305
+ filterSpecificsAgainstEvents: (events) ->
306
+ # If there are no specifics (ie identity) return what we have as-is
307
+ return this unless @hasSpecifics()
308
+
309
+ # Keep and return the rows that refer to any of the provided events (via a leftMost)
310
+ rowsToKeep = _(@rows).select (row) ->
311
+ _(row.leftMostEvents()).any (leftMostEvent) ->
312
+ _(events).any (event) ->
313
+ # We consider events the same if either 1) both have ids and the ids are the same, or 2) both are
314
+ # dates, and the dates are the same
315
+ (event instanceof Date && leftMostEvent instanceof Date && event.getTime() == leftMostEvent.getTime()) ||
316
+ (event.id? && leftMostEvent.id? && event.id == leftMostEvent.id)
317
+
318
+ new hqmf.SpecificOccurrence(rowsToKeep)
319
+
276
320
  hasRow: (row) ->
277
321
  found = false
278
322
  for myRow in @rows
@@ -297,10 +341,14 @@ class hqmf.SpecificOccurrence
297
341
 
298
342
  finalizeEvents: (eventsContext, boundsContext) ->
299
343
  result = this
300
- result = result.intersect(eventsContext) if (eventsContext?)
301
- result = result.intersect(boundsContext) if (boundsContext?)
344
+ result = result.intersect(eventsContext) if eventsContext?
345
+ result = result.intersect(boundsContext) if boundsContext?
302
346
  result.compactReusedEvents()
303
347
 
348
+ # Group rows by everything except the leftmost to apply the subset only to the events from the specific
349
+ # occurrence context rows on the leftmost column. eg for "MOST RECENT: Occurrence A of Lab Result during
350
+ # Occurrence A of Encounter" we want to group by the encounter and apply the most recent to the set of
351
+ # lab results per group (ie encounter)
304
352
  group: ->
305
353
  groupedRows = {}
306
354
  for row in @rows
@@ -308,21 +356,27 @@ class hqmf.SpecificOccurrence
308
356
  groupedRows[row.groupKeyForLeftMost()].push(row)
309
357
  groupedRows
310
358
 
311
- COUNT: (range) ->
312
- @applyRangeSubset(COUNT, range)
359
+ COUNT: (range, fields) ->
360
+ @applyRangeSubset(COUNT, range, fields)
361
+
362
+ MIN: (range, fields) ->
363
+ @applyRangeSubset(MIN, range, fields)
364
+
365
+ MAX: (range, fields) ->
366
+ @applyRangeSubset(MAX, range, fields)
313
367
 
314
- MIN: (range) ->
315
- @applyRangeSubset(MIN, range)
368
+ SUM: (range, fields) ->
369
+ @applyRangeSubset(SUM, range, fields)
316
370
 
317
- MAX: (range) ->
318
- @applyRangeSubset(MAX, range)
371
+ MEDIAN: (range, fields) ->
372
+ @applyRangeSubset(MEDIAN, range, fields)
319
373
 
320
- applyRangeSubset: (func, range) ->
374
+ applyRangeSubset: (func, range, fields) ->
321
375
  return this if !@hasSpecifics()
322
376
  resultRows = []
323
377
  groupedRows = @group()
324
378
  for groupKey, group of groupedRows
325
- if func(hqmf.SpecificsManager.extractEventsForLeftMost(group), range).isTrue()
379
+ if func(hqmf.SpecificsManager.extractEventsForLeftMost(group), range, null, fields).isTrue()
326
380
  resultRows = resultRows.concat(group)
327
381
  new hqmf.SpecificOccurrence(resultRows)
328
382
 
@@ -349,7 +403,7 @@ class hqmf.SpecificOccurrence
349
403
 
350
404
  hasLeftMost: ->
351
405
  for row in @rows
352
- if row.leftMost? || row.tempValue?
406
+ if row.specificLeftMost? || row.nonSpecificLeftMost?
353
407
  return true
354
408
  return false
355
409
 
@@ -373,12 +427,11 @@ class hqmf.SpecificOccurrence
373
427
 
374
428
  class Row
375
429
  # {'OccurrenceAEncounter':1, 'OccurrenceBEncounter'2}
376
- constructor: (leftMost, occurrences={}) ->
377
- throw "left most key must be a string or undefined was: #{leftMost}" if typeof(leftMost) != 'string' and typeof(leftMost) != 'undefined'
430
+ constructor: (specificLeftMost, occurrences={}) ->
378
431
  @length = hqmf.SpecificsManager.occurrences.length
379
432
  @values = []
380
- @leftMost = leftMost
381
- @tempValue = occurrences[undefined]
433
+ @specificLeftMost = specificLeftMost
434
+ @nonSpecificLeftMost = occurrences[undefined]
382
435
  for i in [0...@length]
383
436
  key = hqmf.SpecificsManager.keyLookup[i]
384
437
  value = occurrences[key] || hqmf.SpecificsManager.any
@@ -401,14 +454,25 @@ class Row
401
454
  equals: (other) ->
402
455
  equal = true;
403
456
 
404
- equal &&= Row.valuesEqual(@tempValue, other.tempValue)
457
+ equal &&= Row.valuesEqual(@nonSpecificLeftMost, other.nonSpecificLeftMost)
405
458
  for value,i in @values
406
459
  equal &&= Row.valuesEqual(value, other.values[i])
407
460
  equal
408
461
 
409
- intersect: (other, episodeIndices) ->
410
- intersectedRow = new Row(@leftMost, {})
411
- intersectedRow.tempValue = @tempValue
462
+ intersect: (other, episodeIndices, options = {}) ->
463
+
464
+ # When we're calculating an actual intersection, where we're returning a set of events, we want to make sure that rows that reference
465
+ # disjoint expressions aren't combined; this isn't true if we're calculating a boolean AND, chaining temporal operators, etc
466
+ if options.considerLeftMost
467
+ # If rows being intersected have different leftMost values, with neither null, then the rows reference disjoint expressions and can't be intersected
468
+ return undefined if @specificLeftMost && other.specificLeftMost && !Row.valuesEqual(@specificLeftMost, other.specificLeftMost)
469
+ return undefined if @nonSpecificLeftMost && other.nonSpecificLeftMost && !Row.valuesEqual(@nonSpecificLeftMost, other.nonSpecificLeftMost)
470
+ # We can set the result row to leftMost + tempValue of whichever of row has it set, since they'll either be the same or one will be undefined
471
+ intersectedRow = new Row(@specificLeftMost || other.specificLeftMost, {})
472
+ intersectedRow.nonSpecificLeftMost = @nonSpecificLeftMost || other.nonSpecificLeftMost
473
+ else
474
+ intersectedRow = new Row(@specificLeftMost, {})
475
+ intersectedRow.nonSpecificLeftMost = @nonSpecificLeftMost
412
476
 
413
477
  # if all the episodes are any, then they were not referenced by the parent population. This occurs when an intersection is done
414
478
  # against the identity row. In this case we want to allow the specific occurrences through. This happens when we intersect against a measure
@@ -431,16 +495,20 @@ class Row
431
495
  return true
432
496
 
433
497
  groupKeyForLeftMost: ->
434
- @groupKey(@leftMost)
435
-
436
- groupKey: (key=null) ->
498
+ # Get the key(s) to group by, handling hash of specifics or single specific
499
+ if _.isObject(@specificLeftMost)
500
+ @groupKey(_(@specificLeftMost).chain().values().flatten().value())
501
+ else
502
+ @groupKey([@specificLeftMost])
503
+
504
+ groupKey: (keys) ->
505
+ keys = [keys] if _.isString(keys)
437
506
  keyForGroup = ''
438
507
  for i in [0...@length]
439
- value = hqmf.SpecificsManager.any
440
- value = @values[i].id if @values[i] != hqmf.SpecificsManager.any
441
- if hqmf.SpecificsManager.keyLookup[i] == key
508
+ if _(keys).include(hqmf.SpecificsManager.keyLookup[i])
442
509
  keyForGroup += "X_"
443
510
  else
511
+ value = if @values[i] != hqmf.SpecificsManager.any then @values[i].id else hqmf.SpecificsManager.any
444
512
  keyForGroup += "#{value}_"
445
513
  keyForGroup
446
514
 
@@ -481,8 +549,16 @@ class Row
481
549
  for matchKey in matchKeys
482
550
  occurrences = {}
483
551
  occurrences[entryKey] = entry
484
- occurrences[matchKey] = match
552
+ occurrences[matchKey] = match if matchKey? # We don't want to track RHS unless it's a specific occurrence
485
553
  rows.push(new Row(entryKey, occurrences))
554
+ else
555
+ # Handle case where the match is not a specific occurrence (may have specific occurrences on the RHS)
556
+ nonSpecificLeftMostRows = _(matches.specificContext.rows).select (r) -> r.nonSpecificLeftMost?.id == match.id
557
+ entryOccurrences = {}
558
+ entryOccurrences[entryKey] = entry
559
+ for nonSpecificLeftMostRow in nonSpecificLeftMostRows
560
+ result = nonSpecificLeftMostRow.intersect(new Row(entryKey, entryOccurrences))
561
+ rows.push(result) if result?
486
562
  rows
487
563
 
488
564
  # build specific for a given entry (there are no temporal references)
@@ -503,7 +579,26 @@ class Row
503
579
  result.push(value.id)
504
580
  result
505
581
 
506
-
582
+ toHashKey: ->
583
+ @flattenToIds().join(",") + ",#{@specificLeftMost}" + ",#{@nonSpecificLeftMost?.id}"
584
+
585
+ # If the row references a leftmost, either specific or not, return the event(s)
586
+ # (because a UNION can place multiple events in the specific leftMost, this can be > 1)
587
+ leftMostEvents: ->
588
+ if @nonSpecificLeftMost?
589
+ return [@nonSpecificLeftMost]
590
+ if @specificLeftMost? && _.isString(@specificLeftMost)
591
+ specificIndex = hqmf.SpecificsManager.getColumnIndex(@specificLeftMost)
592
+ return [@values[specificIndex]] if @values[specificIndex]? && @values[specificIndex] != hqmf.SpecificsManager.any
593
+ if @specificLeftMost? && _.isObject(@specificLeftMost)
594
+ events = []
595
+ for id, occurrences of @specificLeftMost
596
+ for occurrence in _.uniq(occurrences)
597
+ specificIndex = hqmf.SpecificsManager.getColumnIndex(occurrence)
598
+ events.push(@values[specificIndex]) if @values[specificIndex]? && @values[specificIndex] != hqmf.SpecificsManager.any
599
+ return events
600
+ return []
601
+
507
602
  @Row = Row
508
603
 
509
604
  ###
@@ -559,4 +654,3 @@ hQuery.CodedEntryList::match = _.wrap(hQuery.CodedEntryList::match, (func, codeS
559
654
  result.specific_occurrence = occurrence
560
655
  return result;
561
656
  );
562
-