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
@@ -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||{}