hqmf2js 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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||{}
|