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.
- checksums.yaml +5 -13
- data/.travis.yml +1 -1
- data/Gemfile +1 -25
- data/Gemfile.lock +170 -146
- data/app/assets/javascripts/crosswalk.js.coffee +17 -19
- data/app/assets/javascripts/custom_calculations.js.coffee +44 -17
- data/app/assets/javascripts/hqmf_util.js.coffee +559 -161
- data/app/assets/javascripts/logging_utils.js.coffee +6 -4
- data/app/assets/javascripts/patient_api_extension.js.coffee +41 -9
- data/app/assets/javascripts/specifics.js.coffee +163 -69
- data/hqmf2js.gemspec +7 -12
- data/lib/assets/javascripts/libraries/map_reduce_utils.js +151 -64
- data/lib/generator/characteristic.js.erb +23 -12
- data/lib/generator/codes_to_json.rb +1 -1
- data/lib/generator/data_criteria.js.erb +15 -3
- data/lib/generator/derived_data.js.erb +5 -0
- data/lib/generator/execution.rb +41 -11
- data/lib/generator/js.rb +74 -41
- data/lib/generator/patient_data.js.erb +1 -1
- data/lib/hqmf2js.rb +0 -1
- data/lib/hquery/engine.rb +3 -1
- data/lib/tasks/convert.rake +20 -12
- data/test/fixtures/NQF59New.json +1423 -0
- data/test/fixtures/fulfills.xml +917 -0
- data/test/fixtures/patients/larry_vanderman.json +573 -654
- data/test/{simplecov.rb → simplecov_init.rb} +0 -0
- data/test/test_helper.rb +2 -3
- data/test/unit/cmd_test.rb +145 -19
- data/test/unit/codes_to_json_test.rb +12 -12
- data/test/unit/custom_calculations_test.rb +2 -6
- data/test/unit/effective_date_test.rb +3 -4
- data/test/unit/erb_context_test.rb +12 -12
- data/test/unit/filter_by_reference_test.rb +39 -0
- data/test/unit/hqmf_from_json_javascript_test.rb +2 -1
- data/test/unit/hqmf_javascript_test.rb +12 -13
- data/test/unit/js_object_test.rb +2 -2
- data/test/unit/library_function_test.rb +210 -42
- data/test/unit/specifics_test.rb +402 -321
- metadata +57 -15
- 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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
|
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) ->
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
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
|
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 =
|
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
|
-
|
599
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
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
|
-
|
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
|
-
|
906
|
-
|
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
|
-
|
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
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
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||{}
|