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
@@ -1,14 +1,28 @@
1
1
  @hqmf.CustomCalc = {}
2
+ @ADE_PRE_V4_ID = ['40280381-454E-C5FA-0145-517F7383016D']
2
3
 
3
4
  @hqmf.CustomCalc.ADE_TTR_OBSERV = (patient, hqmfjs) ->
4
- inrReadings = DURING(hqmfjs.LaboratoryTestResultInr(patient), hqmfjs.MeasurePeriod(patient));
5
+ if (ADE_PRE_V4_ID.indexOf(hqmfjs.hqmf_id)!=-1)
6
+ inrReadings = DURING(hqmfjs.LaboratoryTestResultInr(patient), hqmfjs.MeasurePeriod(patient));
7
+ else
8
+ inrReadings = DURING(hqmfjs.LaboratoryTestPerformedInr(patient), hqmfjs.MeasurePeriod(patient));
5
9
  inrReadings = new hqmf.CustomCalc.PercentTTREntries(inrReadings)
6
- [inrReadings.calculatePercentTTR()]
10
+ return [inrReadings.calculatePercentTTR()]
11
+
12
+ @hqmf.CustomCalc.ADE_TTR_MSRPOPL = (patient, hqmfjs) ->
13
+ if (ADE_PRE_V4_ID.indexOf(hqmfjs.hqmf_id)!=-1)
14
+ inrReadings = DURING(hqmfjs.LaboratoryTestResultInr(patient), hqmfjs.MeasurePeriod(patient));
15
+ else
16
+ inrReadings = DURING(hqmfjs.LaboratoryTestPerformedInr(patient), hqmfjs.MeasurePeriod(patient));
17
+ inrReadings = new hqmf.CustomCalc.PercentTTREntries(inrReadings)
18
+ return new Boolean(inrReadings.calculateNumberOfIntervals() > 1)
7
19
 
8
20
  class @hqmf.CustomCalc.PercentTTREntries extends hQuery.CodedEntryList
9
21
 
10
22
  constructor: (events) ->
11
23
  super()
24
+ @inrRanges = 0
25
+ @totalNumberOfDays = 0
12
26
  @minInr = 2.0
13
27
  @maxInr = 3.0
14
28
  @minOutOfRange = 0.8
@@ -71,17 +85,21 @@ class @hqmf.CustomCalc.PercentTTREntries extends hQuery.CodedEntryList
71
85
 
72
86
 
73
87
  calculateDaysInRange: (firstInr, secondInr) ->
74
-
75
- if ((@belowRange(firstInr) and @belowRange(secondInr)) or (@aboveRange(firstInr) and @aboveRange(secondInr)))
76
- 0
77
- else if (@inRange(firstInr) and @inRange(secondInr))
78
- @differenceInDays(firstInr,secondInr)
79
- else if (@outsideRange(firstInr) and @inRange(secondInr))
80
- @calculateCrossingRange(firstInr,secondInr)
81
- else if (@inRange(firstInr) and @outsideRange(secondInr))
82
- @calculateCrossingRange(secondInr, firstInr)
88
+ if(@differenceInDays(firstInr, secondInr) < 57)
89
+ @inrRanges = @inrRanges + 1
90
+ @totalNumberOfDays = @totalNumberOfDays + @differenceInDays(firstInr, secondInr)
91
+ if ((@belowRange(firstInr) and @belowRange(secondInr)) or (@aboveRange(firstInr) and @aboveRange(secondInr)))
92
+ 0
93
+ else if (@inRange(firstInr) and @inRange(secondInr))
94
+ @differenceInDays(firstInr,secondInr)
95
+ else if (@outsideRange(firstInr) and @inRange(secondInr))
96
+ @calculateCrossingRange(firstInr,secondInr)
97
+ else if (@inRange(firstInr) and @outsideRange(secondInr))
98
+ @calculateCrossingRange(secondInr, firstInr)
99
+ else
100
+ @calculateSpanningRange(firstInr, secondInr)
83
101
  else
84
- @calculateSpanningRange(firstInr, secondInr)
102
+ 0
85
103
 
86
104
  calculateCrossingRange: (outside,inside) ->
87
105
  outsideInr = @inrValue(outside)
@@ -114,9 +132,6 @@ class @hqmf.CustomCalc.PercentTTREntries extends hQuery.CodedEntryList
114
132
  inrValue: (entry) ->
115
133
  entry.values()[0].scalar()
116
134
 
117
- totalNumberOfDays: () ->
118
- @differenceInDays(this[0],this[this.length-1])
119
-
120
135
  calculateTTR: () ->
121
136
  total = 0
122
137
  for left, i in this
@@ -125,6 +140,18 @@ class @hqmf.CustomCalc.PercentTTREntries extends hQuery.CodedEntryList
125
140
  total += @calculateDaysInRange(left, right)
126
141
  total
127
142
 
143
+ calculateNumberOfIntervals: () ->
144
+ total = 0
145
+ for left, i in this
146
+ if (i < this.length-1)
147
+ right = this[i+1]
148
+ if(@differenceInDays(left, right) < 57)
149
+ total = total + 1
150
+ total
151
+
128
152
  calculatePercentTTR: () ->
129
- @calculateTTR()/@totalNumberOfDays()*100
130
-
153
+ @totalNumberOfDays = 0
154
+ if (@calculateNumberOfIntervals() > 1)
155
+ @calculateTTR()/@totalNumberOfDays*100
156
+ else
157
+ return
@@ -1,6 +1,6 @@
1
1
  # Represents an HL7 timestamp
2
2
  class TS
3
-
3
+
4
4
  # Create a new TS instance
5
5
  # hl7ts - an HL7 TS value as a string, e.g. 20121023131023 for
6
6
  # Oct 23, 2012 at 13:10:23.
@@ -18,7 +18,7 @@ class TS
18
18
  @date = new Date(Date.UTC(year, month, day, hour, minute))
19
19
  else
20
20
  @date = new Date()
21
-
21
+
22
22
  # Add a time period to th and return it
23
23
  # pq - a time period as an instance of PQ. Supports units of a (year), mo (month),
24
24
  # wk (week), d (day), h (hour) and min (minute).
@@ -36,13 +36,13 @@ class TS
36
36
  else if pq.unit=="min"
37
37
  @date.setUTCMinutes(@date.getUTCMinutes()+pq.value)
38
38
  else
39
- throw "Unknown time unit: "+pq.unit
39
+ throw new Error("Unknown time unit: "+pq.unit)
40
40
  this
41
-
41
+
42
42
  # Returns the difference between this TS and the supplied TS as an absolute
43
43
  # number using the supplied granularity. E.g. if granularity is specified as year
44
44
  # then it will return the number of years between this TS and the supplied TS.
45
- # granularity - specifies the granularity of the difference. Supports units
45
+ # granularity - specifies the granularity of the difference. Supports units
46
46
  # of a (year), mo (month), wk (week), d (day), h (hour) and min (minute).
47
47
  difference: (ts, granularity) ->
48
48
  earlier = later = null
@@ -66,14 +66,14 @@ class TS
66
66
  else if granularity=="min"
67
67
  TS.minutesDifference(earlier,later)
68
68
  else
69
- throw "Unknown time unit: "+granularity
70
-
69
+ throw new Error("Unknown time unit: "+granularity)
70
+
71
71
  # Get the value of this TS as a JS Date
72
72
  asDate: ->
73
73
  @date
74
-
74
+
75
75
  # Returns whether this TS is before the supplied TS ignoring seconds
76
- before: (other) ->
76
+ before: (other) ->
77
77
  if @date==null || other.date==null
78
78
  return false
79
79
  if other.inclusive
@@ -93,10 +93,10 @@ class TS
93
93
  a.getTime() > b.getTime()
94
94
 
95
95
  equals: (other) ->
96
- (@date==null && other.date==null) || (@date.getTime()==other.date.getTime())
96
+ (@date==null && other.date==null) || (@date!=null && other.date!=null && @date.getTime()==other.date.getTime())
97
97
 
98
98
  # Returns whether this TS is before or concurrent with the supplied TS ignoring seconds
99
- beforeOrConcurrent: (other) ->
99
+ beforeOrConcurrent: (other) ->
100
100
  if @date==null || other.date==null
101
101
  return false
102
102
  [a,b] = TS.dropSeconds(@date, other.date)
@@ -108,13 +108,13 @@ class TS
108
108
  return false
109
109
  [a,b] = TS.dropSeconds(@date, other.date)
110
110
  a.getTime() >= b.getTime()
111
-
111
+
112
112
  # Return whether this TS and the supplied TS are within the same minute (i.e.
113
113
  # same timestamp when seconds are ignored)
114
114
  withinSameMinute: (other) ->
115
115
  [a,b] = TS.dropSeconds(@date, other.date)
116
116
  a.getTime()==b.getTime()
117
-
117
+
118
118
  # Number of whole years between the two time stamps (as Date objects)
119
119
  @yearsDifference: (earlier, later) ->
120
120
  if (later.getUTCMonth() < earlier.getUTCMonth())
@@ -125,34 +125,34 @@ class TS
125
125
  later.getUTCFullYear()-earlier.getUTCFullYear()-1
126
126
  else
127
127
  later.getUTCFullYear()-earlier.getUTCFullYear()
128
-
128
+
129
129
  # Number of whole months between the two time stamps (as Date objects)
130
130
  @monthsDifference: (earlier, later) ->
131
131
  if (later.getUTCDate() >= earlier.getUTCDate())
132
132
  (later.getUTCFullYear()-earlier.getUTCFullYear())*12+later.getUTCMonth()-earlier.getUTCMonth()
133
133
  else
134
134
  (later.getUTCFullYear()-earlier.getUTCFullYear())*12+later.getUTCMonth()-earlier.getUTCMonth()-1
135
-
135
+
136
136
  # Number of whole minutes between the two time stamps (as Date objects)
137
137
  @minutesDifference: (earlier, later) ->
138
138
  [e,l] = TS.dropSeconds(earlier,later)
139
139
  Math.floor(((l.getTime()-e.getTime())/1000)/60)
140
-
140
+
141
141
  # Number of whole hours between the two time stamps (as Date objects)
142
142
  @hoursDifference: (earlier, later) ->
143
143
  Math.floor(TS.minutesDifference(earlier,later)/60)
144
-
144
+
145
145
  # Number of days betweem the two time stamps (as Date objects)
146
146
  @daysDifference: (earlier, later) ->
147
147
  # have to discard time portion for day difference calculation purposes
148
148
  e = new Date(Date.UTC(earlier.getUTCFullYear(), earlier.getUTCMonth(), earlier.getUTCDate()))
149
149
  l = new Date(Date.UTC(later.getUTCFullYear(), later.getUTCMonth(), later.getUTCDate()))
150
150
  Math.floor(TS.hoursDifference(e,l)/24)
151
-
151
+
152
152
  # Number of whole weeks between the two time stmaps (as Date objects)
153
153
  @weeksDifference: (earlier, later) ->
154
154
  Math.floor(TS.daysDifference(earlier,later)/7)
155
-
155
+
156
156
  # Drop the seconds from the supplied timeStamps (as Date objects)
157
157
  # returns the new time stamps with seconds set to 0 as an array
158
158
  @dropSeconds: (timeStamps...) ->
@@ -173,6 +173,8 @@ fieldOrContainerValue = (value, fieldName, defaultToValue=true) ->
173
173
  value[fieldName]()
174
174
  else if typeof value[fieldName] != 'undefined'
175
175
  value[fieldName]
176
+ else if value.json? && typeof value.json[fieldName] != 'undefined'
177
+ value.json[fieldName]
176
178
  else if defaultToValue
177
179
  value
178
180
  else
@@ -184,7 +186,7 @@ fieldOrContainerValue = (value, fieldName, defaultToValue=true) ->
184
186
  # Represents an HL7 CD value
185
187
  class CD
186
188
  constructor: (@code, @system) ->
187
-
189
+
188
190
  # Returns whether the supplied code matches this one.
189
191
  match: (codeOrHash) ->
190
192
  # We might be passed a simple code value like "M" or a CodedEntry
@@ -199,11 +201,11 @@ class CD
199
201
  else
200
202
  c1==c2
201
203
  @CD = CD
202
-
203
- # Represents a list of codes
204
+
205
+ # Represents a list of codes
204
206
  class CodeList
205
207
  constructor: (@codes) ->
206
-
208
+
207
209
  # Returns whether the supplied code matches any of the contained codes
208
210
  match: (codeOrHash) ->
209
211
  # We might be passed a simple code value like "M" or a CodedEntry
@@ -223,14 +225,14 @@ class CodeList
223
225
  result = true
224
226
  result
225
227
  @CodeList = CodeList
226
-
228
+
227
229
  # Represents and HL7 physical quantity
228
230
  class PQ
229
231
  constructor: (@value, @unit, @inclusive=true) ->
230
-
232
+
231
233
  # Helper method to make a PQ behave like a patient API value
232
234
  scalar: -> @value
233
-
235
+
234
236
  # Returns whether this is less than the supplied value
235
237
  lessThan: (scalarOrHash) ->
236
238
  val = fieldOrContainerValue(scalarOrHash, 'scalar')
@@ -256,38 +258,80 @@ class PQ
256
258
  greaterThanOrEqual: (scalarOrHash) ->
257
259
  val = fieldOrContainerValue(scalarOrHash, 'scalar')
258
260
  @value>=val
259
-
261
+
260
262
  # Returns whether this is equal to the supplied value or hash
261
263
  match: (scalarOrHash) ->
262
264
  val = fieldOrContainerValue(scalarOrHash, 'scalar')
263
265
  @value==val
266
+
267
+ # Helper method to normalize the current value as a new PQ with 'min' precision
268
+ normalizeToMins: ->
269
+ TIME_UNITS =
270
+ a: 'years'
271
+ mo: 'months'
272
+ wk: 'weeks'
273
+ d: 'days'
274
+ h: 'hours'
275
+ min: 'minutes'
276
+ s: 'seconds'
277
+ TIME_UNITS_MAP =
278
+ a: 365 * 24 * 60
279
+ mo: 30 * 24 * 60
280
+ wk: 7 * 24 * 60
281
+ d: 24 * 60
282
+ h: 60
283
+ min: 1
284
+ s: 1/60
285
+ return unless TIME_UNITS[@unit]?
286
+ # use minutes as the default precision
287
+ new PQ(@value * TIME_UNITS_MAP[@unit], 'min', @inclusive)
264
288
  @PQ = PQ
265
-
289
+
266
290
  # Represents an HL7 interval
267
291
  class IVL_PQ
268
292
  # Create a new instance, must supply either a lower or upper bound and if both
269
293
  # are supplied the units must match.
270
294
  constructor: (@low_pq, @high_pq) ->
271
295
  if !@low_pq && !@high_pq
272
- throw "Must have a lower or upper bound"
296
+ throw new Error("Must have a lower or upper bound")
273
297
  if @low_pq && @low_pq.unit && @high_pq && @high_pq.unit && @low_pq.unit != @high_pq.unit
274
- throw "Mismatched low and high units: "+@low_pq.unit+", "+@high_pq.unit
298
+ throw new Error("Mismatched low and high units: "+@low_pq.unit+", "+@high_pq.unit)
275
299
  unit: ->
276
300
  if @low_pq
277
301
  @low_pq.unit
278
302
  else
279
303
  @high_pq.unit
280
-
304
+
281
305
  # Return whether the supplied scalar or patient API hash value is within this range
282
306
  match: (scalarOrHash) ->
283
307
  val = fieldOrContainerValue(scalarOrHash, 'scalar')
284
- (!@low_pq? || @low_pq.lessThan(val)) && (!@high_pq? || @high_pq.greaterThan(val))
308
+ #Add a check for ANYNonNull value for Reference Range High And Low (Lab Test). QDM 4.2 update.
309
+ if @low_pq? && @low_pq.constructor == ANYNonNull
310
+ val != null
311
+ else if @high_pq? && @high_pq.constructor == ANYNonNull
312
+ val != null
313
+ else
314
+ (!@low_pq? || @low_pq.lessThan(val)) && (!@high_pq? || @high_pq.greaterThan(val))
315
+
316
+ # Helper method to normalize the current values as a new IVL_PQ with 'min' precision
317
+ normalizeToMins: -> new IVL_PQ(@low_pq?.normalizeToMins(), @high_pq?.normalizeToMins())
285
318
  @IVL_PQ = IVL_PQ
286
-
319
+
287
320
  # Represents an HL7 time interval
288
321
  class IVL_TS
289
322
  constructor: (@low, @high) ->
290
-
323
+
324
+ # support comparison to another Date for static dates
325
+ match: (other) ->
326
+ return false unless other?
327
+ other = getTS(other, @low?.inclusive || @high?.inclusive)
328
+ if @low && @low.inclusive && @high && @high.inclusive
329
+ @low.equals(other) && @high.equals(other)
330
+ else if @low
331
+ @low.before(other)
332
+ else if @high
333
+ @high.after(other)
334
+
291
335
  # add an offset to the upper and lower bounds
292
336
  add: (pq) ->
293
337
  if @low
@@ -295,116 +339,160 @@ class IVL_TS
295
339
  if @high
296
340
  @high.add(pq)
297
341
  this
298
-
342
+
299
343
  # During: this low is after other low and this high is before other high
300
344
  DURING: (other) -> this.SDU(other) && this.EDU(other)
301
-
345
+
302
346
  # Overlap: this overlaps with other
303
- OVERLAP: (other) -> this.SDU(other) || this.EDU(other) || (this.SBS(other) && this.EAE(other))
304
-
347
+ OVERLAP: (other) ->
348
+ if @high.date == null && other.high.date == null
349
+ true # If neither have ends, they inherently overlap on the timeline
350
+ else if @high.date == null
351
+ !this.SAE(other)
352
+ else if other.high.date == null
353
+ !this.EBS(other)
354
+ else
355
+ this.SDU(other) || this.EDU(other) || (this.SBS(other) && this.EAE(other))
356
+
305
357
  # Concurrent: this low and high are the same as other low and high
306
358
  CONCURRENT: (other) -> this.SCW(other) && this.ECW(other)
307
-
359
+
308
360
  # Starts Before Start: this low is before other low
309
- SBS: (other) ->
361
+ SBS: (other) ->
310
362
  if @low && other.low
311
363
  @low.before(other.low)
312
364
  else
313
365
  false
314
-
366
+
315
367
  # Starts After Start: this low is after other low
316
- SAS: (other) ->
368
+ SAS: (other) ->
317
369
  if @low && other.low
318
370
  @low.after(other.low)
319
371
  else
320
372
  false
321
-
373
+
322
374
  # Starts Before End: this low is before other high
323
375
  SBE: (other) ->
324
376
  if @low && other.high
325
377
  @low.before(other.high)
326
378
  else
327
379
  false
328
-
380
+
329
381
  # Starts After End: this low is after other high
330
- SAE: (other) ->
382
+ SAE: (other) ->
331
383
  if @low && other.high
332
384
  @low.after(other.high)
333
385
  else
334
386
  false
335
-
387
+
388
+ # Starts During: this low is between other low and high
389
+ SDU: (other) ->
390
+ if @low && other.low && other.high
391
+ @low.afterOrConcurrent(other.low) && @low.beforeOrConcurrent(other.high)
392
+ else
393
+ false
394
+
395
+ #starts before or during: this low is less than the other low or the other high.
396
+ #if other does not have a high or does not have a low this will return false
397
+ SBDU: (other) ->
398
+ this.SBS(other) || this.SDU(other)
399
+
400
+ # Starts Concurrent With: this low is the same as other low ignoring seconds
401
+ SCW: (other) ->
402
+ if @low && other.low
403
+ @low.asDate() && other.low.asDate() && @low.withinSameMinute(other.low)
404
+ else
405
+ false
406
+
407
+ # Starts Concurrent With End: this low is the same as other high ignoring seconds
408
+ SCWE: (other) ->
409
+ if @low && other.high
410
+ @low.asDate() && other.high.asDate() && @low.withinSameMinute(other.high)
411
+ else
412
+ false
413
+
414
+ #Starts Before or Concurrent with: this low is <= other low
415
+ SBCW: (other) ->
416
+ this.SBS(other) || this.SCW(other)
417
+
418
+ SBCWE: (other) ->
419
+ this.SBE(other) || this.SCWE(other)
420
+ # Starts After or Concurrent with other: this low is >= other low
421
+ SACW: (other) ->
422
+ this.SAS(other) || this.SCW(other)
423
+
424
+ # Starts After or Concurrent with End : This low is >= other high
425
+ SACWE: (other) ->
426
+ this.SAE(other) || this.SCWE(other)
427
+
428
+
336
429
  # Ends Before Start: this high is before other low
337
430
  EBS: (other) ->
338
431
  if @high && other.low
339
432
  @high.before(other.low)
340
433
  else
341
434
  false
342
-
435
+
343
436
  # Ends After Start: this high is after other low
344
- EAS: (other) ->
437
+ EAS: (other) ->
345
438
  if @high && other.low
346
439
  @high.after(other.low)
347
440
  else
348
441
  false
349
-
442
+
350
443
  # Ends Before End: this high is before other high
351
- EBE: (other) ->
444
+ EBE: (other) ->
352
445
  if @high && other.high
353
446
  @high.before(other.high)
354
447
  else
355
448
  false
356
-
449
+
357
450
  # Ends After End: this high is after other high
358
451
  EAE: (other) ->
359
452
  if @high && other.high
360
453
  @high.after(other.high)
361
454
  else
362
455
  false
363
-
364
- # Starts During: this low is between other low and high
365
- SDU: (other) ->
366
- if @low && other.low && other.high
367
- @low.afterOrConcurrent(other.low) && @low.beforeOrConcurrent(other.high)
368
- else
369
- false
370
-
456
+
371
457
  # Ends During: this high is between other low and high
372
- EDU: (other) ->
458
+ EDU: (other) ->
373
459
  if @high && other.low && other.high
374
460
  @high.afterOrConcurrent(other.low) && @high.beforeOrConcurrent(other.high)
375
461
  else
376
462
  false
377
-
463
+
378
464
  # Ends Concurrent With: this high is the same as other high ignoring seconds
379
- ECW: (other) ->
465
+ ECW: (other) ->
380
466
  if @high && other.high
381
467
  @high.asDate() && other.high.asDate() && @high.withinSameMinute(other.high)
382
468
  else
383
469
  false
384
-
385
- # Starts Concurrent With: this low is the same as other low ignoring seconds
386
- SCW: (other) ->
387
- if @low && other.low
388
- @low.asDate() && other.low.asDate() && @low.withinSameMinute(other.low)
389
- else
390
- false
391
-
470
+
392
471
  # Ends Concurrent With Start: this high is the same as other low ignoring seconds
393
472
  ECWS: (other) ->
394
473
  if @high && other.low
395
474
  @high.asDate() && other.low.asDate() && @high.withinSameMinute(other.low)
396
475
  else
397
476
  false
398
-
399
- # Starts Concurrent With End: this low is the same as other high ignoring seconds
400
- SCWE: (other) ->
401
- if @low && other.high
402
- @low.asDate() && other.high.asDate() && @low.withinSameMinute(other.high)
403
- else
404
- false
477
+
478
+ EBDU: (other) ->
479
+ this.EBS(other) || this.EDU(other)
480
+
481
+ EBCW: (other) ->
482
+ this.EBE(other) || this.ECW(other)
483
+
484
+ EACW: (other) ->
485
+ this.EAE(other) || this.ECW(other)
486
+
487
+ EBCWS: (other) ->
488
+ this.EBS(other) || this.ECWS(other)
489
+
490
+ EACWS: (other) ->
491
+ this.EAS(other) || this.ECWS(other)
405
492
 
406
493
  equals: (other) ->
407
- (@low==null && other.low==null) || (@low.equals(other.low)) && (@high==null && other.high==null) || (@high.equals(other.high))
494
+ ((@low == null && other.low == null) || (@low != null && @low.equals(other.low))) &&
495
+ ((@high == null && other.high == null) || (@high != null && @high.equals(other.high)))
408
496
 
409
497
  @IVL_TS = IVL_TS
410
498
 
@@ -432,7 +520,7 @@ evalUnlessShortCircuit = (fn) ->
432
520
  invokeAll = (patient, initialSpecificContext, fns) ->
433
521
  (invokeOne(patient, initialSpecificContext, fn) for fn in fns)
434
522
  @invokeAll = invokeAll
435
-
523
+
436
524
  # Returns true if one or more of the supplied values is true
437
525
  atLeastOneTrue = (precondition, patient, initialSpecificContext, valueFns...) ->
438
526
  evalUnlessShortCircuit ->
@@ -447,7 +535,7 @@ allTrue = (precondition, patient, initialSpecificContext, valueFns...) ->
447
535
  values = []
448
536
  for valueFn in valueFns
449
537
  value = invokeOne(patient, initialSpecificContext, valueFn)
450
- # break if the we have a false value and we're short circuiting.
538
+ # break if the we have a false value and we're short circuiting.
451
539
  #If we're not short circuiting then we want to calculate everything
452
540
  break if value.isFalse() && Logger.short_circuit
453
541
  values.push(value)
@@ -466,7 +554,7 @@ allTrue = (precondition, patient, initialSpecificContext, valueFns...) ->
466
554
 
467
555
 
468
556
  @allTrue = allTrue
469
-
557
+
470
558
  # Returns true if one or more of the supplied values is false
471
559
  atLeastOneFalse = (precondition, patient, initialSpecificContext, valueFns...) ->
472
560
  # values = invokeAll(patient, initialSpecificContext, valueFns)
@@ -483,7 +571,7 @@ atLeastOneFalse = (precondition, patient, initialSpecificContext, valueFns...) -
483
571
  break if Logger.short_circuit
484
572
  hqmf.SpecificsManager.intersectAll(new Boolean(values.length>0 && hasFalse), values, true)
485
573
  @atLeastOneFalse = atLeastOneFalse
486
-
574
+
487
575
  # Returns true if all of the supplied values are false
488
576
  allFalse = (precondition, patient, initialSpecificContext, valueFns...) ->
489
577
  evalUnlessShortCircuit ->
@@ -491,7 +579,7 @@ allFalse = (precondition, patient, initialSpecificContext, valueFns...) ->
491
579
  falseValues = (value for value in values when value.isFalse())
492
580
  hqmf.SpecificsManager.unionAll(new Boolean(falseValues.length>0 && falseValues.length==values.length), values, true)
493
581
  @allFalse = allFalse
494
-
582
+
495
583
  # Return true if compareTo matches value
496
584
  matchingValue = (value, compareTo) ->
497
585
  new Boolean(compareTo.match(value))
@@ -513,10 +601,42 @@ filterEventsByValue = (events, value) ->
513
601
  filterEventsByField = (events, field, value) ->
514
602
  respondingEvents = (event for event in events when event.respondTo(field))
515
603
  unit = value.unit() if value.unit?
516
- result = (event for event in respondingEvents when value.match(event[field](unit)))
604
+ result = []
605
+ for event in respondingEvents
606
+ # If the responding event's field has multiple attributes, check each one
607
+ if event[field](unit) instanceof Array
608
+ for attr in event[field](unit)
609
+ if value.match(attr)
610
+ result.push event
611
+ break
612
+ else
613
+ result.push event if value.match(event[field](unit))
517
614
  hqmf.SpecificsManager.maintainSpecifics(result, events)
518
615
  @filterEventsByField = filterEventsByField
519
616
 
617
+ #Function that grabs events with the correct Communication Direction
618
+ filterEventsByCommunicationDirection = (events, value) ->
619
+ matchingEvents = (event for event in events when (event.json.direction == value))
620
+ hqmf.SpecificsManager.maintainSpecifics(matchingEvents, events)
621
+ @filterEventsByCommunicationDirection = filterEventsByCommunicationDirection
622
+
623
+ # This turns out to work similarly to eventsMatchBounds
624
+ filterEventsByReference = (events, type, possibles) ->
625
+ specificContext = new hqmf.SpecificOccurrence()
626
+ matching = []
627
+ matching.specific_occurrence = events.specific_occrrence
628
+ for event in events when event.respondTo("references")
629
+ referencedIds = (item.referenced_id().valueOf() for item in event.referencesByType(type))
630
+ matchingPossibles = (possible for possible in possibles when possible.id.valueOf() in referencedIds)
631
+ matching.push(event) if matchingPossibles.length > 0
632
+ if events.specific_occurrence? || possibles.specific_occurrence?
633
+ specificContext.addRows(Row.buildRowsForMatching(events.specific_occurrence, event, possibles.specific_occurrence, matchingPossibles))
634
+ else
635
+ specificContext.addIdentityRow()
636
+ matching.specificContext = specificContext.finalizeEvents(events.specificContext, possibles.specificContext)
637
+ matching
638
+ @filterEventsByReference = filterEventsByReference
639
+
520
640
  shiftTimes = (event, field) ->
521
641
  shiftedEvent = new event.constructor(event.json)
522
642
  shiftedEvent.setTimestamp(shiftedEvent[field]())
@@ -558,10 +678,22 @@ denormalizeEventsByLocation = (events, field) ->
558
678
  hqmf.SpecificsManager.maintainSpecifics(result, events)
559
679
  @denormalizeEventsByLocation = denormalizeEventsByLocation
560
680
 
681
+ # Creates a new set of events with one location per event. Input events with more than
682
+ # one location will be duplicated once per location and each resulting event will
683
+ # be assigned one location. Start and end times of the event will be adjusted to match the
684
+ # value of the supplied field
685
+ denormalizeEventsByTransfer = (events, field) ->
686
+ respondingEvents = (event for event in events when event.respondTo(field) and event[field]())
687
+ denormalizedEvents = (denormalizeEvent(event) for event in respondingEvents)
688
+ denormalizedEvents = [].concat denormalizedEvents...
689
+ result = adjustBoundsForField(denormalizedEvents, 'transferTime')
690
+ hqmf.SpecificsManager.maintainSpecifics(result, events)
691
+ @denormalizeEventsByTransfer = denormalizeEventsByTransfer
692
+
561
693
  # Utility method to obtain the value set for an OID
562
694
  getCodes = (oid) ->
563
695
  codes = OidDictionary[oid]
564
- throw "value set oid could not be found: #{oid}" unless codes?
696
+ throw new Error("value set oid could not be found: #{oid}") unless codes?
565
697
  codes
566
698
  @getCodes = getCodes
567
699
 
@@ -580,28 +712,49 @@ class CrossProduct extends Array
580
712
  @specific_occurrence[event.id] = eventList.specific_occurrence if eventList.specific_occurrence
581
713
  listCount: -> @eventLists.length
582
714
  childList: (index) -> @eventLists[index]
715
+ intersect: ->
716
+ result = @childList(0) || []
717
+ for index in [1...@listCount()] by 1
718
+ currentIds = @childList(index).map((event) -> event.id)
719
+ result = result.filter((event) -> currentIds.indexOf(event.id) >= 0)
720
+ result
583
721
 
584
722
  # Create a CrossProduct of the supplied event lists.
585
723
  XPRODUCT = (eventLists...) ->
586
- hqmf.SpecificsManager.intersectAll(new CrossProduct(eventLists), eventLists)
724
+ hqmf.SpecificsManager.intersectAll(new CrossProduct(eventLists), eventLists, false, null, considerLeftMost: true)
587
725
  @XPRODUCT = XPRODUCT
588
726
 
589
727
  # Create a new list containing all the events from the supplied event lists
590
728
  UNION = (eventLists...) ->
591
729
  union = []
592
- # keep track of the specific occurrences by encounter ID. This is used in
730
+ # keep track of the specific occurrences by encounter ID. This is used in
593
731
  # eventsMatchBounds (specifically in buildRowsForMatching down the _.isObject path)
594
732
  specific_occurrence = {}
595
733
  for eventList in eventLists
596
734
  for event in eventList
597
735
  if eventList.specific_occurrence
598
- specific_occurrence[event.id] ||= []
599
- specific_occurrence[event.id].push eventList.specific_occurrence
736
+ # If there's already an object due to a previous UNION, merge the contents
737
+ if _.isObject(eventList.specific_occurrence)
738
+ for id, occurrences of eventList.specific_occurrence
739
+ specific_occurrence[id] ||= []
740
+ specific_occurrence[id] = _.uniq(specific_occurrence[id].concat(occurrences))
741
+ else
742
+ specific_occurrence[event.id] ||= []
743
+ specific_occurrence[event.id].push eventList.specific_occurrence
600
744
  union.push(event)
601
745
  union.specific_occurrence = specific_occurrence unless _.isEmpty(specific_occurrence)
602
746
  hqmf.SpecificsManager.unionAll(union, eventLists)
603
747
  @UNION = UNION
604
748
 
749
+ # Create a CrossProduct of the supplied event lists.
750
+ INTERSECT = (eventLists...) ->
751
+ events = hqmf.SpecificsManager.intersectAll((new CrossProduct(eventLists)).intersect(), eventLists, false, null, considerLeftMost: true)
752
+ # If the logical evaluation of an INTERSECT excludes an event, the resulting specifics should not include
753
+ # rows that refer to that event; this fixes https://jira.oncprojectracking.org/browse/BONNIE-64
754
+ events.specificContext = events.specificContext.filterSpecificsAgainstEvents(events)
755
+ events
756
+ @INTERSECT = INTERSECT
757
+
605
758
  # Return true if the number of events matches the supplied range
606
759
  COUNT = (events, range) ->
607
760
  count = events.length
@@ -619,7 +772,17 @@ getIVL = (eventOrTimeStamp) ->
619
772
  new IVL_TS(ts, ts)
620
773
  @getIVL = getIVL
621
774
 
622
- eventAccessor = {
775
+ # Convert any JS Date into a TS
776
+ getTS = (date, inclusive=false) ->
777
+ if date.asDate
778
+ date
779
+ else
780
+ ts = new TS(null, inclusive)
781
+ ts.date = date
782
+ ts
783
+ @getTS = getTS
784
+
785
+ eventAccessor = {
623
786
  'DURING': 'low',
624
787
  'OVERLAP': 'low',
625
788
  'SBS': 'low',
@@ -636,11 +799,21 @@ eventAccessor = {
636
799
  'SCW': 'low',
637
800
  'ECWS': 'high'
638
801
  'SCWE': 'low',
802
+ 'SBCW': 'low',
803
+ 'SBCWE': 'low',
804
+ 'SACW': 'low',
805
+ 'SACWE': 'low',
806
+ 'SBDU': 'low',
807
+ 'EBCW': 'high',
808
+ 'EBCWS': 'high',
809
+ 'EACW': 'high',
810
+ 'EACWS': 'high',
811
+ 'EADU': 'high',
639
812
  'CONCURRENT': 'low',
640
813
  'DATEDIFF': 'low'
641
814
  }
642
815
 
643
- boundAccessor = {
816
+ boundAccessor = {
644
817
  'DURING': 'low',
645
818
  'OVERLAP': 'low',
646
819
  'SBS': 'low',
@@ -657,10 +830,20 @@ boundAccessor = {
657
830
  'SCW': 'low',
658
831
  'ECWS': 'low'
659
832
  'SCWE': 'high',
833
+ 'SBCW': 'low',
834
+ 'SBCWE': 'high',
835
+ 'SACW': 'low',
836
+ 'SACWE': 'high',
837
+ 'SBDU': 'high',
838
+ 'EBCW': 'high',
839
+ 'EBCWS': 'low',
840
+ 'EACW': 'high',
841
+ 'EACWS': 'low',
842
+ 'EADU': 'low',
660
843
  'CONCURRENT': 'low',
661
844
  'DATEDIFF': 'low'
662
845
  }
663
-
846
+
664
847
  # Determine whether the supplied event falls within range of the supplied bound
665
848
  # using the method to determine which property of the event and bound to use in
666
849
  # the comparison. E.g. if method is SBS then check whether the start of the event
@@ -670,7 +853,7 @@ withinRange = (method, eventIVL, boundIVL, range) ->
670
853
  boundTS = boundIVL[boundAccessor[method]]
671
854
  range.match(eventTS.difference(boundTS, range.unit()))
672
855
  @withinRange = withinRange
673
-
856
+
674
857
  # Determine which bounds an event matches
675
858
  eventMatchesBounds = (event, bounds, methodName, range) ->
676
859
  if bounds.eventLists
@@ -692,19 +875,22 @@ eventMatchesBounds = (event, bounds, methodName, range) ->
692
875
  ))
693
876
  hqmf.SpecificsManager.maintainSpecifics(matchingBounds, bounds)
694
877
  @eventMatchesBounds = eventMatchesBounds
695
-
878
+
696
879
  # Determine which event match one of the supplied bounds
697
880
  eventsMatchBounds = (events, bounds, methodName, range) ->
698
881
  if (bounds.length==undefined)
699
882
  bounds = [bounds]
700
883
  if (events.length==undefined)
701
884
  events = [events]
702
-
885
+
703
886
  specificContext = new hqmf.SpecificOccurrence()
704
- hasSpecificOccurrence = (events.specific_occurrence? || bounds.specific_occurrence?)
887
+ # For the bounds (RHS), we check not only if the immediate RHS has specifics, but also whether anything on
888
+ # the RHS has specifics steps further removed, by checking if there's a specificContext with specifics
889
+ hasSpecificOccurrence = (events.specific_occurrence? || bounds.specific_occurrence? || bounds.specificContext?.hasSpecifics())
705
890
  matchingEvents = []
706
891
  matchingEvents.specific_occurrence = events.specific_occurrence
707
892
  for event in events
893
+ continue unless event
708
894
  matchingBounds=eventMatchesBounds(event, bounds, methodName, range)
709
895
  matchingEvents.push(event) if matchingBounds.length > 0
710
896
 
@@ -715,12 +901,12 @@ eventsMatchBounds = (events, bounds, methodName, range) ->
715
901
  else
716
902
  # add all stars
717
903
  specificContext.addIdentityRow()
718
-
904
+
719
905
  matchingEvents.specificContext = specificContext.finalizeEvents(events.specificContext, bounds.specificContext)
720
-
906
+
721
907
  matchingEvents
722
908
  @eventsMatchBounds = eventsMatchBounds
723
-
909
+
724
910
  DURING = (events, bounds, offset) ->
725
911
  eventsMatchBounds(events, bounds, "DURING", offset)
726
912
  @DURING = DURING
@@ -772,7 +958,7 @@ EDU = (events, bounds, offset) ->
772
958
  ECW = (events, bounds, offset) ->
773
959
  eventsMatchBounds(events, bounds, "ECW", offset)
774
960
  @ECW = ECW
775
-
961
+
776
962
  SCW = (events, bounds, offset) ->
777
963
  eventsMatchBounds(events, bounds, "SCW", offset)
778
964
  @SCW = SCW
@@ -780,11 +966,50 @@ SCW = (events, bounds, offset) ->
780
966
  ECWS = (events, bounds, offset) ->
781
967
  eventsMatchBounds(events, bounds, "ECWS", offset)
782
968
  @ECWS = ECWS
783
-
969
+
784
970
  SCWE = (events, bounds, offset) ->
785
971
  eventsMatchBounds(events, bounds, "SCWE", offset)
786
972
  @SCWE = SCWE
787
973
 
974
+ EBDU = (events, bounds, offset) ->
975
+ eventsMatchBounds(events, bounds, "EBDU", offset)
976
+ @EBDU = EBDU
977
+
978
+ EBCW = (events, bounds, offset) ->
979
+ eventsMatchBounds(events, bounds, "EBCW", offset)
980
+ @EBCW = EBCW
981
+ EACW = (events, bounds, offset) ->
982
+ eventsMatchBounds(events, bounds, "EACW", offset)
983
+ @EACW =EACW
984
+
985
+ EBCWS = (events, bounds, offset) ->
986
+ eventsMatchBounds(events, bounds, "EBCWS", offset)
987
+ @EBCWS = EBCWS
988
+
989
+ EACWS = (events, bounds, offset) ->
990
+ eventsMatchBounds(events, bounds, "EACWS", offset)
991
+ @EACWS = EACWS
992
+
993
+ SBDU= (events, bounds, offset) ->
994
+ eventsMatchBounds(events, bounds, "SBDU", offset)
995
+ @SBDU = SBDU
996
+
997
+ SBCW= (events, bounds, offset) ->
998
+ eventsMatchBounds(events, bounds, "SBCW", offset)
999
+ @SBCW = SBCW
1000
+
1001
+ SBCWE= (events, bounds, offset) ->
1002
+ eventsMatchBounds(events, bounds, "SBCWE", offset)
1003
+ @SBCWE = SBCWE
1004
+
1005
+ SACW= (events, bounds, offset) ->
1006
+ eventsMatchBounds(events, bounds, "SACW", offset)
1007
+ @SACW = SACW
1008
+
1009
+ SACWE= (events, bounds, offset) ->
1010
+ eventsMatchBounds(events, bounds, "SACWE", offset)
1011
+ @SACWE = SACWE
1012
+
788
1013
  CONCURRENT = (events, bounds, offset) ->
789
1014
  eventsMatchBounds(events, bounds, "CONCURRENT", offset)
790
1015
  @CONCURRENT = CONCURRENT
@@ -797,12 +1022,12 @@ dateSortAscending = (a, b) ->
797
1022
  a.timeStamp().getTime() - b.timeStamp().getTime()
798
1023
  @dateSortAscending = dateSortAscending
799
1024
 
800
- applySpecificOccurrenceSubset = (operator, result, range, calculateSpecifics) ->
1025
+ applySpecificOccurrenceSubset = (operator, result, range, fields) ->
801
1026
  # the subset operators are re-used in the specifics calculation of those operators. Checking for a specificContext
802
1027
  # prevents entering into an infinite loop here.
803
1028
  if (result.specificContext?)
804
1029
  if (range?)
805
- result.specificContext = result.specificContext[operator](range)
1030
+ result.specificContext = result.specificContext[operator](range, fields)
806
1031
  else
807
1032
  result.specificContext = result.specificContext[operator]()
808
1033
  result
@@ -816,55 +1041,60 @@ uniqueEvents = (events) ->
816
1041
  # if we have multiple events at the same exact time and they happen to be the one selected by FIRST, RECENT, etc
817
1042
  # then we want to select all of these issues as the first, most recent, etc.
818
1043
  selectConcurrent = (target, events) ->
819
- targetIVL = target.asIVL_TS()
820
- uniqueEvents((result for result in events when result.asIVL_TS().equals(targetIVL)))
1044
+ uniqueEvents((result for result in events when target.timeStamp().getTime() == result.timeStamp().getTime()))
821
1045
  @selectConcurrent = selectConcurrent
822
1046
 
823
- FIRST = (events) ->
824
- result = []
825
- result = selectConcurrent(events.sort(dateSortAscending)[0], events) if (events.length > 0)
826
- applySpecificOccurrenceSubset('FIRST',hqmf.SpecificsManager.maintainSpecifics(result, events))
1047
+ # Common code for all subset operators
1048
+ applySubsetOperator = (operatorName, events, sortFunction, subsetIndex) ->
1049
+ # If we have a specificContext, and there are actual specific occurrences involved (ie the specificContext
1050
+ # has rows other than identity), then we have to return all the events that *might* satisfy the subset
1051
+ # operator once specific occurrences are taken into account
1052
+ if events.specificContext && events.specificContext.hasSpecifics()
1053
+
1054
+ # Start by calculating the specific context subset, which creates at least one row for each event that
1055
+ # satisfies the subset operator for at least one of the specifics
1056
+ events.specificContext = events.specificContext[operatorName]()
1057
+
1058
+ # Then, return only the events that can satisfy the subset operator for one or more specifics
1059
+ return hqmf.SpecificsManager.filterEventsAgainstSpecifics(events)
1060
+
1061
+ else
1062
+
1063
+ # There's is no specific context, and that means that either there are no specifics involved or we are
1064
+ # being called recursively from within the specifics handling code; in each case we just perform the
1065
+ # logical operator and return the appropriate subset elements
1066
+ result = []
1067
+ result = selectConcurrent(events.sort(sortFunction)[subsetIndex], events) if (events.length > subsetIndex)
1068
+ hqmf.SpecificsManager.maintainSpecifics(result, events)
1069
+ return result
1070
+
1071
+
1072
+ FIRST = (events) -> applySubsetOperator('FIRST', events, dateSortAscending, 0)
827
1073
  @FIRST = FIRST
828
1074
 
829
- SECOND = (events) ->
830
- result = []
831
- result = selectConcurrent(events.sort(dateSortAscending)[1], events) if (events.length > 1)
832
- applySpecificOccurrenceSubset('SECOND',hqmf.SpecificsManager.maintainSpecifics(result, events))
1075
+ SECOND = (events) -> applySubsetOperator('SECOND', events, dateSortAscending, 1)
833
1076
  @SECOND = SECOND
834
1077
 
835
- THIRD = (events) ->
836
- result = []
837
- result = selectConcurrent(events.sort(dateSortAscending)[2], events) if (events.length > 2)
838
- applySpecificOccurrenceSubset('THIRD',hqmf.SpecificsManager.maintainSpecifics(result, events))
1078
+ THIRD = (events) -> applySubsetOperator('THIRD', events, dateSortAscending, 2)
839
1079
  @THIRD = THIRD
840
1080
 
841
- FOURTH = (events) ->
842
- result = []
843
- result = selectConcurrent(events.sort(dateSortAscending)[3], events) if (events.length > 3)
844
- applySpecificOccurrenceSubset('FOURTH',hqmf.SpecificsManager.maintainSpecifics(result, events))
1081
+ FOURTH = (events) -> applySubsetOperator('FOURTH', events, dateSortAscending, 3)
845
1082
  @FOURTH = FOURTH
846
1083
 
847
- FIFTH = (events) ->
848
- result = []
849
- result = selectConcurrent(events.sort(dateSortAscending)[4], events) if (events.length > 4)
850
- applySpecificOccurrenceSubset('FIFTH',hqmf.SpecificsManager.maintainSpecifics(result, events))
1084
+ FIFTH = (events) -> applySubsetOperator('FIFTH', events, dateSortAscending, 4)
851
1085
  @FIFTH = FIFTH
852
1086
 
853
- RECENT = (events) ->
854
- result = []
855
- result = selectConcurrent(events.sort(dateSortDescending)[0], events) if (events.length > 0)
856
- applySpecificOccurrenceSubset('RECENT',hqmf.SpecificsManager.maintainSpecifics(result, events))
1087
+ RECENT = (events) -> applySubsetOperator('RECENT', events, dateSortDescending, 0)
857
1088
  @RECENT = RECENT
858
-
859
- LAST = (events) ->
860
- RECENT(events)
1089
+
1090
+ LAST = (events) -> RECENT(events)
861
1091
  @LAST = LAST
862
-
1092
+
863
1093
  valueSortDescending = (a, b) ->
864
1094
  va = vb = Infinity
865
1095
  if a.value
866
1096
  va = a.value()["scalar"]
867
- if b.value
1097
+ if b.value
868
1098
  vb = b.value()["scalar"]
869
1099
  if va==vb
870
1100
  0
@@ -876,7 +1106,7 @@ valueSortAscending = (a, b) ->
876
1106
  va = vb = Infinity
877
1107
  if a.value
878
1108
  va = a.value()["scalar"]
879
- if b.value
1109
+ if b.value
880
1110
  vb = b.value()["scalar"]
881
1111
  if va==vb
882
1112
  0
@@ -884,26 +1114,72 @@ valueSortAscending = (a, b) ->
884
1114
  va - vb
885
1115
  @valueSortAscending = valueSortAscending
886
1116
 
887
- MIN = (events, range) ->
1117
+ FIELD_METHOD_UNITS = {
1118
+ 'cumulativeMedicationDuration': 'd'
1119
+ 'lengthOfStay': 'd'
1120
+ }
1121
+
1122
+ MIN = (events, range, fields) ->
888
1123
  minValue = Infinity
889
1124
  if (events.length > 0)
890
1125
  minValue = events.sort(valueSortAscending)[0].value()["scalar"]
891
1126
  result = new Boolean(range.match(minValue))
892
- applySpecificOccurrenceSubset('MIN',hqmf.SpecificsManager.maintainSpecifics(result, events), range)
1127
+ applySpecificOccurrenceSubset('MIN',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields)
893
1128
  @MIN = MIN
894
1129
 
895
- MAX = (events, range) ->
1130
+ MAX = (events, range, fields) ->
896
1131
  maxValue = -Infinity
897
1132
  if (events.length > 0)
898
1133
  maxValue = events.sort(valueSortDescending)[0].value()["scalar"]
899
1134
  result = new Boolean(range.match(maxValue))
900
- applySpecificOccurrenceSubset('MAX',hqmf.SpecificsManager.maintainSpecifics(result, events), range)
1135
+ applySpecificOccurrenceSubset('MAX',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields)
901
1136
  @MAX = MAX
902
1137
 
1138
+ SUM = (events, range, initialSpecificContext, fields) ->
1139
+ sum = 0
1140
+ comparison = range
1141
+ field = fields?[0]
1142
+ field = 'values' if field == 'result'
1143
+ if (events.length > 0)
1144
+ if field
1145
+ unit = FIELD_METHOD_UNITS[field] || 'd'
1146
+ if field == 'values'
1147
+ sum += event[field]()['scalar'] for event in events
1148
+ else
1149
+ sum += event[field]() for event in events
1150
+ sum = (new PQ(sum, unit, true)).normalizeToMins()
1151
+ comparison = comparison.normalizeToMins()
1152
+ result = new Boolean(comparison.match(sum))
1153
+ applySpecificOccurrenceSubset('SUM',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields)
1154
+ @SUM = SUM
1155
+
1156
+ MEDIAN = (events, range, initialSpecificContext, fields) ->
1157
+ median = Infinity
1158
+ comparison = range
1159
+ field = fields?[0]
1160
+ field = 'values' if field == 'result'
1161
+ if (events.length > 0)
1162
+ if field
1163
+ unit = FIELD_METHOD_UNITS[field] || 'd'
1164
+ if field == 'values'
1165
+ values = ( event[field]()['scalar'] for event in events )
1166
+ else
1167
+ values = ( event[field]() for event in events )
1168
+ sorted = _.clone(values).sort((f,s) -> f-s)
1169
+ median = if sorted.length%2 then sorted[Math.floor(sorted.length/2)] else (sorted[sorted.length/2-1]+sorted[sorted.length/2]) /2
1170
+ if field != 'values'
1171
+ median = (new PQ(median, unit, true)).normalizeToMins()
1172
+ comparison = comparison.normalizeToMins()
1173
+ result = new Boolean(comparison.match(median))
1174
+ applySpecificOccurrenceSubset('MEDIAN',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields)
1175
+ @MEDIAN = MEDIAN
1176
+
903
1177
  DATEDIFF = (events, range) ->
904
1178
  return hqmf.SpecificsManager.maintainSpecifics(new Boolean(false), events) if events.length < 2
905
- throw "cannot calculate against more than 2 events" if events.length > 2
906
- hqmf.SpecificsManager.maintainSpecifics(new Boolean(withinRange('DATEDIFF', getIVL(events[0]), getIVL(events[1]), range)), events)
1179
+ events = events.sort(dateSortAscending)
1180
+ # events are now sorted, DATEDIFF is between first and last event
1181
+ # throw "cannot calculate against more than 2 events" if events.length > 2
1182
+ hqmf.SpecificsManager.maintainSpecifics(new Boolean(withinRange('DATEDIFF', getIVL(events[0]), getIVL(events[events.length - 1]), range)), events)
907
1183
  @DATEDIFF = DATEDIFF
908
1184
 
909
1185
  # Calculate the set of time differences in minutes between pairs of events
@@ -913,31 +1189,153 @@ DATEDIFF = (events, range) ->
913
1189
  # combination of events
914
1190
  TIMEDIFF = (events, range, initialSpecificContext) ->
915
1191
  if events.listCount() != 2
916
- throw "TIMEDIFF can only process 2 lists of events"
1192
+ # handle nested events for Unions
1193
+ if events.length >= 2
1194
+ event1 = events.sort(dateSortAscending)[0]
1195
+ event2 = events.sort(dateSortAscending)[events.length - 1]
1196
+ return [event1.asTS().difference(event2.asTS(), 'min')]
1197
+ else
1198
+ throw new Error("TIMEDIFF can only process 2 lists of events")
917
1199
  eventList1 = events.childList(0)
918
1200
  eventList2 = events.childList(1)
919
- eventIndex1 = hqmf.SpecificsManager.getColumnIndex(eventList1.specific_occurrence)
920
- eventIndex2 = hqmf.SpecificsManager.getColumnIndex(eventList2.specific_occurrence)
921
- eventMap1 = {}
922
- eventMap2 = {}
923
- for event in eventList1
924
- eventMap1[event.id] = event
925
- for event in eventList2
926
- eventMap2[event.id] = event
927
- results = []
928
- for row in initialSpecificContext.rows
929
- event1 = row.values[eventIndex1]
930
- event2 = row.values[eventIndex2]
931
- if event1 and event2 and event1 != hqmf.SpecificsManager.any and event2 != hqmf.SpecificsManager.any
932
- # The maps contain the actual events we want to work with since these may contain
933
- # time shifted clones of the events in the specificContext, e.g. via adjustBoundsForField
934
- shiftedEvent1 = eventMap1[event1.id]
935
- shiftedEvent2 = eventMap2[event2.id]
936
- if shiftedEvent1 and shiftedEvent2
937
- results.push(shiftedEvent1.asTS().difference(shiftedEvent2.asTS(), 'min'))
1201
+ eventIndex1 = hqmf.SpecificsManager.getColumnIndex(eventList1.specific_occurrence) if eventList1.specific_occurrence
1202
+ eventIndex2 = hqmf.SpecificsManager.getColumnIndex(eventList2.specific_occurrence) if eventList2.specific_occurrence
1203
+ if (eventIndex1? && eventIndex2?)
1204
+ eventMap1 = {}
1205
+ eventMap2 = {}
1206
+ for event in eventList1
1207
+ eventMap1[event.id] = event
1208
+ for event in eventList2
1209
+ eventMap2[event.id] = event
1210
+ results = []
1211
+ for row in initialSpecificContext.rows
1212
+ event1 = row.values[eventIndex1]
1213
+ event2 = row.values[eventIndex2]
1214
+ if event1 and event2 and event1 != hqmf.SpecificsManager.any and event2 != hqmf.SpecificsManager.any
1215
+ # The maps contain the actual events we want to work with since these may contain
1216
+ # time shifted clones of the events in the specificContext, e.g. via adjustBoundsForField
1217
+ shiftedEvent1 = eventMap1[event1.id]
1218
+ shiftedEvent2 = eventMap2[event2.id]
1219
+ if shiftedEvent1 and shiftedEvent2
1220
+ results.push(shiftedEvent1.asTS().difference(shiftedEvent2.asTS(), 'min'))
1221
+ else
1222
+ if (eventList1.length > 0 && eventList2.length > 0)
1223
+ event1 = eventList1.sort(dateSortAscending)[0]
1224
+ event2 = eventList2.sort(dateSortAscending)[eventList2.length - 1]
1225
+ results = [event1.asTS().difference(event2.asTS(), 'min')]
938
1226
  results
939
1227
  @TIMEDIFF = TIMEDIFF
940
1228
 
1229
+ DATETIMEDIFF = (events, range, initialSpecificContext) ->
1230
+ if range
1231
+ DATEDIFF(events, range)
1232
+ else
1233
+ TIMEDIFF(events, range, initialSpecificContext)
1234
+ @DATETIMEDIFF = DATETIMEDIFF
1235
+
1236
+ #used to collect a number of days a series of date ranges may be active: Such as overlap issues with CMD
1237
+ class ActiveDays
1238
+
1239
+ constructor: ->
1240
+ @active_days=[]
1241
+
1242
+ reset: ->
1243
+ @active_days=[]
1244
+
1245
+ add_ivlts: (ivlts)->
1246
+ @add_range(ivlts.low,ivlts.high)
1247
+
1248
+ add_range: (low,high) ->
1249
+ start = @as_date(low)
1250
+ end = @as_date(high)
1251
+ days = (end.getTime()-start.getTime())/(1000*60*60*24) #number of days between dates
1252
+ days += 1 # the above calculation only accounts for the days between and up to the end need to add the start back on
1253
+ @add_days_from(start,days)
1254
+
1255
+ add_days_from: (start, number_of_days) ->
1256
+ for x in[0..number_of_days-1]
1257
+ diff = (1000*60*60*24)*x
1258
+ @add_date(new Date((start.getTime() + diff)))
1259
+
1260
+
1261
+ add_date: (_date)->
1262
+ date = @as_date(_date)
1263
+ formated_date = @format_date(date)
1264
+ if @active_days[formated_date]
1265
+ @active_days[formated_date]["count"] +=1
1266
+ else
1267
+ @active_days[formated_date]={date: formated_date, count: 1}
1268
+
1269
+ days_active: (low,high)->
1270
+ start = @as_date(low)
1271
+ end = @as_date(high)
1272
+ formated_start = @format_date(start)
1273
+ formated_end = @format_date(end)
1274
+ days = @active_days.slice(formated_start,formated_end+1).filter (e)-> e # the filter removes all nulls,0, and empty strings
1275
+ days
1276
+
1277
+ format_date: (date)->
1278
+ #format the date as an integer in the format of yyyymmdd ex 20141010
1279
+ ds = ""+date.getFullYear()
1280
+ month = date.getMonth() + 1
1281
+ day = date.getDate()
1282
+ ds += if month < 10 then "0"+ month else month
1283
+ ds += if day < 10 then "0"+ day else day
1284
+ parseInt(ds)
1285
+
1286
+ date_diff: (low,high) ->
1287
+
1288
+
1289
+ as_date: (date)->
1290
+ if date instanceof TS
1291
+ new Date(date.asDate().getTime())
1292
+ else
1293
+ new Date(date.getTime())
1294
+
1295
+ print_days_in_range: (low,high) ->
1296
+ start = @as_date(low)
1297
+ end = @as_date(high)
1298
+ formated_start = @format_date(start)
1299
+ formated_end = @format_date(end)
1300
+ str = "Start :"+start.toString()+"\n"
1301
+ str+= "End :"+end.toString()+"\n"
1302
+ str += "Start :"+formated_start+"\n"
1303
+ str+= "End :"+formated_end+"\n"
1304
+ for x in @days_active(low,high)
1305
+ str+="Date: "+x["date"]+ " count: "+x["count"]+"\n"
1306
+ str
1307
+
1308
+ @ActiveDays = ActiveDays
1309
+
1310
+
1311
+ class CMD extends ActiveDays
1312
+
1313
+ constructor:(@medications,@calculation_type) ->
1314
+ super()
1315
+ for m in @medications
1316
+ @add_medication(m)
1317
+
1318
+ add_medication: (medication) ->
1319
+ dose = medication.dose().scalar
1320
+ dosesPerDay = medication.administrationTiming().dosesPerDay()
1321
+ if @calculation_type == "order"
1322
+ for oi in medication.orderInformation()
1323
+ totalDays = oi.quantityOrdered().value()/dose/dosesPerDay
1324
+ if !isNaN(totalDays)
1325
+ startDate = new Date(oi.orderDateTime())
1326
+ fills = oi.fills() || 1
1327
+ @add_days_from(startDate,totalDays*fills)
1328
+ else
1329
+ history = medication.fulfillmentHistory()
1330
+ for fh in history
1331
+
1332
+ totalDays = fh.quantityDispensed().value()/dose/dosesPerDay
1333
+ if !isNaN(totalDays)
1334
+ startDate = new Date(fh.dispenseDate())
1335
+ @add_days_from(startDate,totalDays)
1336
+
1337
+ @CMD = CMD
1338
+
941
1339
  @OidDictionary = {};
942
1340
 
943
1341
  hqmfjs = hqmfjs||{}