hqmf2js 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
-