hqmf2js 1.0.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.
- data/.gitignore +10 -0
- data/.travis.yml +17 -0
- data/Gemfile +41 -0
- data/Gemfile.lock +202 -0
- data/README.md +7 -0
- data/Rakefile +22 -0
- data/VERSION +1 -0
- data/app/assets/javascripts/hqmf_util.js.coffee +776 -0
- data/app/assets/javascripts/logging_utils.js.coffee +150 -0
- data/app/assets/javascripts/patient_api_extension.js.coffee +36 -0
- data/app/assets/javascripts/specifics.js.coffee +462 -0
- data/bin/hqmf2js.rb +25 -0
- data/config/warble.rb +144 -0
- data/hqmf2js.gemspec +20 -0
- data/lib/config/codes.xml +1935 -0
- data/lib/generator/characteristic.js.erb +19 -0
- data/lib/generator/codes_to_json.rb +81 -0
- data/lib/generator/converter.rb +60 -0
- data/lib/generator/data_criteria.js.erb +47 -0
- data/lib/generator/derived_data.js.erb +5 -0
- data/lib/generator/js.rb +263 -0
- data/lib/generator/measure_period.js.erb +18 -0
- data/lib/generator/patient_data.js.erb +22 -0
- data/lib/generator/population_criteria.js.erb +4 -0
- data/lib/generator/precondition.js.erb +14 -0
- data/lib/hqmf2js.rb +20 -0
- data/lib/hquery/engine.rb +4 -0
- data/lib/tasks/codes.rake +12 -0
- data/lib/tasks/coffee.rake +15 -0
- data/lib/tasks/convert.rake +47 -0
- data/lib/tasks/cover_me.rake +8 -0
- data/test/fixtures/NQF59New.xml +1047 -0
- data/test/fixtures/codes/codes.xls +0 -0
- data/test/fixtures/codes/codes.xml +1941 -0
- data/test/fixtures/i2b2.xml +305 -0
- data/test/fixtures/invalid/missing_id.xml +18 -0
- data/test/fixtures/invalid/unknown_criteria_type.xml +16 -0
- data/test/fixtures/invalid/unknown_demographic_entry.xml +16 -0
- data/test/fixtures/invalid/unknown_population_type.xml +9 -0
- data/test/fixtures/invalid/unknown_value_type.xml +18 -0
- data/test/fixtures/js/59New.js +366 -0
- data/test/fixtures/js/test1.js +356 -0
- data/test/fixtures/js/test2.js +366 -0
- data/test/fixtures/json/0043.json +6 -0
- data/test/fixtures/json/0043_hqmf1.json +1 -0
- data/test/fixtures/json/0043_hqmf2.json +172 -0
- data/test/fixtures/json/59New.json +1352 -0
- data/test/fixtures/patient_api.js +2823 -0
- data/test/fixtures/patients/francis_drake.json +1180 -0
- data/test/fixtures/patients/larry_vanderman.json +645 -0
- data/test/test_helper.rb +58 -0
- data/test/unit/codes_to_json_test.rb +38 -0
- data/test/unit/effective_date_test.rb +48 -0
- data/test/unit/hqmf_from_json_javascript_test.rb +108 -0
- data/test/unit/hqmf_javascript_test.rb +175 -0
- data/test/unit/library_function_test.rb +553 -0
- data/test/unit/specifics_test.rb +757 -0
- metadata +183 -0
@@ -0,0 +1,776 @@
|
|
1
|
+
# Represents an HL7 timestamp
|
2
|
+
class TS
|
3
|
+
|
4
|
+
# Create a new TS instance
|
5
|
+
# hl7ts - an HL7 TS value as a string, e.g. 20121023131023 for
|
6
|
+
# Oct 23, 2012 at 13:10:23.
|
7
|
+
constructor: (hl7ts, @inclusive=false) ->
|
8
|
+
if hl7ts
|
9
|
+
year = parseInt(hl7ts.substring(0, 4))
|
10
|
+
month = parseInt(hl7ts.substring(4, 6), 10)-1
|
11
|
+
day = parseInt(hl7ts.substring(6, 8), 10)
|
12
|
+
hour = parseInt(hl7ts.substring(8, 10), 10)
|
13
|
+
if isNaN(hour)
|
14
|
+
hour = 0
|
15
|
+
minute = parseInt(hl7ts.substring(10,12), 10)
|
16
|
+
if isNaN(minute)
|
17
|
+
minute = 0
|
18
|
+
@date = new Date(year, month, day, hour, minute)
|
19
|
+
else
|
20
|
+
@date = new Date()
|
21
|
+
|
22
|
+
# Add a time period to th and return it
|
23
|
+
# pq - a time period as an instance of PQ. Supports units of a (year), mo (month),
|
24
|
+
# wk (week), d (day), h (hour) and min (minute).
|
25
|
+
add: (pq) ->
|
26
|
+
if pq.unit=="a"
|
27
|
+
@date.setFullYear(@date.getFullYear()+pq.value)
|
28
|
+
else if pq.unit=="mo"
|
29
|
+
@date.setMonth(@date.getMonth()+pq.value)
|
30
|
+
else if pq.unit=="wk"
|
31
|
+
@date.setDate(@date.getDate()+(7*pq.value))
|
32
|
+
else if pq.unit=="d"
|
33
|
+
@date.setDate(@date.getDate()+pq.value)
|
34
|
+
else if pq.unit=="h"
|
35
|
+
@date.setHours(@date.getHours()+pq.value)
|
36
|
+
else if pq.unit=="min"
|
37
|
+
@date.setMinutes(@date.getMinutes()+pq.value)
|
38
|
+
else
|
39
|
+
throw "Unknown time unit: "+pq.unit
|
40
|
+
this
|
41
|
+
|
42
|
+
# Returns the difference between this TS and the supplied TS as an absolute
|
43
|
+
# number using the supplied granularity. E.g. if granularity is specified as year
|
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
|
46
|
+
# of a (year), mo (month), wk (week), d (day), h (hour) and min (minute).
|
47
|
+
difference: (ts, granularity) ->
|
48
|
+
earlier = later = null
|
49
|
+
if @afterOrConcurrent(ts)
|
50
|
+
earlier = ts.asDate()
|
51
|
+
later = @date
|
52
|
+
else
|
53
|
+
earlier = @date
|
54
|
+
later = ts.asDate()
|
55
|
+
if granularity=="a"
|
56
|
+
TS.yearsDifference(earlier,later)
|
57
|
+
else if granularity=="mo"
|
58
|
+
TS.monthsDifference(earlier,later)
|
59
|
+
else if granularity=="wk"
|
60
|
+
TS.weeksDifference(earlier,later)
|
61
|
+
else if granularity=="d"
|
62
|
+
TS.daysDifference(earlier,later)
|
63
|
+
else if granularity=="h"
|
64
|
+
TS.hoursDifference(earlier,later)
|
65
|
+
else if granularity=="min"
|
66
|
+
TS.minutesDifference(earlier,later)
|
67
|
+
else
|
68
|
+
throw "Unknown time unit: "+granularity
|
69
|
+
|
70
|
+
# Get the value of this TS as a JS Date
|
71
|
+
asDate: ->
|
72
|
+
@date
|
73
|
+
|
74
|
+
# Returns whether this TS is before the supplied TS ignoring seconds
|
75
|
+
before: (other) ->
|
76
|
+
if @date==null || other.date==null
|
77
|
+
return false
|
78
|
+
if other.inclusive
|
79
|
+
beforeOrConcurrent(other)
|
80
|
+
else
|
81
|
+
[a,b] = TS.dropSeconds(@date, other.date)
|
82
|
+
a.getTime() < b.getTime()
|
83
|
+
|
84
|
+
# Returns whether this TS is after the supplied TS ignoring seconds
|
85
|
+
after: (other) ->
|
86
|
+
if @date==null || other.date==null
|
87
|
+
return false
|
88
|
+
if other.inclusive
|
89
|
+
afterOrConcurrent(other)
|
90
|
+
else
|
91
|
+
[a,b] = TS.dropSeconds(@date, other.date)
|
92
|
+
a.getTime() > b.getTime()
|
93
|
+
|
94
|
+
# Returns whether this TS is before or concurrent with the supplied TS ignoring seconds
|
95
|
+
beforeOrConcurrent: (other) ->
|
96
|
+
if @date==null || other.date==null
|
97
|
+
return false
|
98
|
+
[a,b] = TS.dropSeconds(@date, other.date)
|
99
|
+
a.getTime() <= b.getTime()
|
100
|
+
|
101
|
+
# Returns whether this TS is after or concurrent with the supplied TS ignoring seconds
|
102
|
+
afterOrConcurrent: (other) ->
|
103
|
+
if @date==null || other.date==null
|
104
|
+
return false
|
105
|
+
[a,b] = TS.dropSeconds(@date, other.date)
|
106
|
+
a.getTime() >= b.getTime()
|
107
|
+
|
108
|
+
# Return whether this TS and the supplied TS are within the same minute (i.e.
|
109
|
+
# same timestamp when seconds are ignored)
|
110
|
+
withinSameMinute: (other) ->
|
111
|
+
[a,b] = TS.dropSeconds(@date, other.date)
|
112
|
+
a.getTime()==b.getTime()
|
113
|
+
|
114
|
+
# Number of whole years between the two time stamps (as Date objects)
|
115
|
+
@yearsDifference: (earlier, later) ->
|
116
|
+
if (later.getMonth() < earlier.getMonth())
|
117
|
+
later.getFullYear()-earlier.getFullYear()-1
|
118
|
+
else if (later.getMonth() == earlier.getMonth() && later.getDate() >= earlier.getDate())
|
119
|
+
later.getFullYear()-earlier.getFullYear()
|
120
|
+
else if (later.getMonth() == earlier.getMonth() && later.getDate() < earlier.getDate())
|
121
|
+
later.getFullYear()-earlier.getFullYear()-1
|
122
|
+
else
|
123
|
+
later.getFullYear()-earlier.getFullYear()
|
124
|
+
|
125
|
+
# Number of whole months between the two time stamps (as Date objects)
|
126
|
+
@monthsDifference: (earlier, later) ->
|
127
|
+
if (later.getDate() >= earlier.getDate())
|
128
|
+
(later.getFullYear()-earlier.getFullYear())*12+later.getMonth()-earlier.getMonth()
|
129
|
+
else
|
130
|
+
(later.getFullYear()-earlier.getFullYear())*12+later.getMonth()-earlier.getMonth()-1
|
131
|
+
|
132
|
+
# Number of whole minutes between the two time stamps (as Date objects)
|
133
|
+
@minutesDifference: (earlier, later) ->
|
134
|
+
Math.floor(((later.getTime()-earlier.getTime())/1000)/60)
|
135
|
+
|
136
|
+
# Number of whole hours between the two time stamps (as Date objects)
|
137
|
+
@hoursDifference: (earlier, later) ->
|
138
|
+
Math.floor(TS.minutesDifference(earlier,later)/60)
|
139
|
+
|
140
|
+
# Number of days betweem the two time stamps (as Date objects)
|
141
|
+
@daysDifference: (earlier, later) ->
|
142
|
+
# have to discard time portion for day difference calculation purposes
|
143
|
+
e = new Date(earlier.getFullYear(), earlier.getMonth(), earlier.getDate())
|
144
|
+
e.setUTCHours(0)
|
145
|
+
l = new Date(later.getFullYear(), later.getMonth(), later.getDate())
|
146
|
+
l.setUTCHours(0)
|
147
|
+
Math.floor(TS.hoursDifference(e,l)/24)
|
148
|
+
|
149
|
+
# Number of whole weeks between the two time stmaps (as Date objects)
|
150
|
+
@weeksDifference: (earlier, later) ->
|
151
|
+
Math.floor(TS.daysDifference(earlier,later)/7)
|
152
|
+
|
153
|
+
# Drop the seconds from the supplied timeStamps (as Date objects)
|
154
|
+
# returns the new time stamps with seconds set to 0 as an array
|
155
|
+
@dropSeconds: (timeStamps...) ->
|
156
|
+
timeStampsNoSeconds = for timeStamp in timeStamps
|
157
|
+
noSeconds = new Date(timeStamp.getTime())
|
158
|
+
noSeconds.setSeconds(0)
|
159
|
+
noSeconds
|
160
|
+
timeStampsNoSeconds
|
161
|
+
@TS = TS
|
162
|
+
|
163
|
+
# Utility function used to extract data from a supplied object, hash or simple value
|
164
|
+
# First looks for an accessor function, then an object property or hash key. If
|
165
|
+
# defaultToValue is specified it will return the supplied value if neither an accessor
|
166
|
+
# or hash entry exists, if false it will return null.
|
167
|
+
fieldOrContainerValue = (value, fieldName, defaultToValue=true) ->
|
168
|
+
if value?
|
169
|
+
if typeof value[fieldName] == 'function'
|
170
|
+
value[fieldName]()
|
171
|
+
else if typeof value[fieldName] != 'undefined'
|
172
|
+
value[fieldName]
|
173
|
+
else if defaultToValue
|
174
|
+
value
|
175
|
+
else
|
176
|
+
null
|
177
|
+
else
|
178
|
+
null
|
179
|
+
@fieldOrContainerValue = fieldOrContainerValue
|
180
|
+
|
181
|
+
# Represents an HL7 CD value
|
182
|
+
class CD
|
183
|
+
constructor: (@code, @system) ->
|
184
|
+
|
185
|
+
# Returns whether the supplied code matches this one.
|
186
|
+
match: (codeOrHash) ->
|
187
|
+
# We might be passed a simple code value like "M" or a CodedEntry
|
188
|
+
# Do our best to get a code value but only get a code system name if one is
|
189
|
+
# supplied
|
190
|
+
codeToMatch = fieldOrContainerValue(codeOrHash, 'code')
|
191
|
+
systemToMatch = fieldOrContainerValue(codeOrHash, 'codeSystemName', false)
|
192
|
+
c1 = hQuery.CodedValue.normalize(codeToMatch)
|
193
|
+
c2 = hQuery.CodedValue.normalize(@code)
|
194
|
+
if @system && systemToMatch
|
195
|
+
c1==c2 && @system==systemToMatch
|
196
|
+
else
|
197
|
+
c1==c2
|
198
|
+
@CD = CD
|
199
|
+
|
200
|
+
# Represents a list of codes
|
201
|
+
class CodeList
|
202
|
+
constructor: (@codes) ->
|
203
|
+
|
204
|
+
# Returns whether the supplied code matches any of the contained codes
|
205
|
+
match: (codeOrHash) ->
|
206
|
+
# We might be passed a simple code value like "M" or a CodedEntry
|
207
|
+
# Do our best to get a code value but only get a code system name if one is
|
208
|
+
# supplied
|
209
|
+
codeToMatch = fieldOrContainerValue(codeOrHash, 'code')
|
210
|
+
c1 = hQuery.CodedValue.normalize(codeToMatch)
|
211
|
+
systemToMatch = fieldOrContainerValue(codeOrHash, 'codeSystemName', false)
|
212
|
+
result = false
|
213
|
+
for codeSystemName, codeList of @codes
|
214
|
+
for code in codeList
|
215
|
+
c2 = hQuery.CodedValue.normalize(code)
|
216
|
+
if codeSystemName && systemToMatch # check that code systems match if both specified
|
217
|
+
if c1==c2 && codeSystemName==systemToMatch
|
218
|
+
result = true
|
219
|
+
else if c1==c2 # no code systems to match to just match codes
|
220
|
+
result = true
|
221
|
+
result
|
222
|
+
@CodeList = CodeList
|
223
|
+
|
224
|
+
# Represents and HL7 physical quantity
|
225
|
+
class PQ
|
226
|
+
constructor: (@value, @unit, @inclusive=true) ->
|
227
|
+
|
228
|
+
# Helper method to make a PQ behave like a patient API value
|
229
|
+
scalar: -> @value
|
230
|
+
|
231
|
+
# Returns whether this is less than the supplied value
|
232
|
+
lessThan: (scalarOrHash) ->
|
233
|
+
val = fieldOrContainerValue(scalarOrHash, 'scalar')
|
234
|
+
if @inclusive
|
235
|
+
@lessThanOrEqual(val)
|
236
|
+
else
|
237
|
+
@value<val
|
238
|
+
|
239
|
+
# Returns whether this is greater than the supplied value
|
240
|
+
greaterThan: (scalarOrHash) ->
|
241
|
+
val = fieldOrContainerValue(scalarOrHash, 'scalar')
|
242
|
+
if @inclusive
|
243
|
+
@greaterThanOrEqual(val)
|
244
|
+
else
|
245
|
+
@value>val
|
246
|
+
|
247
|
+
# Returns whether this is less than or equal to the supplied value
|
248
|
+
lessThanOrEqual: (scalarOrHash) ->
|
249
|
+
val = fieldOrContainerValue(scalarOrHash, 'scalar')
|
250
|
+
@value<=val
|
251
|
+
|
252
|
+
# Returns whether this is greater than or equal to the supplied value
|
253
|
+
greaterThanOrEqual: (scalarOrHash) ->
|
254
|
+
val = fieldOrContainerValue(scalarOrHash, 'scalar')
|
255
|
+
@value>=val
|
256
|
+
|
257
|
+
# Returns whether this is equal to the supplied value or hash
|
258
|
+
match: (scalarOrHash) ->
|
259
|
+
val = fieldOrContainerValue(scalarOrHash, 'scalar')
|
260
|
+
@value==val
|
261
|
+
@PQ = PQ
|
262
|
+
|
263
|
+
# Represents an HL7 interval
|
264
|
+
class IVL_PQ
|
265
|
+
# Create a new instance, must supply either a lower or upper bound and if both
|
266
|
+
# are supplied the units must match.
|
267
|
+
constructor: (@low_pq, @high_pq) ->
|
268
|
+
if !@low_pq && !@high_pq
|
269
|
+
throw "Must have a lower or upper bound"
|
270
|
+
if @low_pq && @low_pq.unit && @high_pq && @high_pq.unit && @low_pq.unit != @high_pq.unit
|
271
|
+
throw "Mismatched low and high units: "+@low_pq.unit+", "+@high_pq.unit
|
272
|
+
unit: ->
|
273
|
+
if @low_pq
|
274
|
+
@low_pq.unit
|
275
|
+
else
|
276
|
+
@high_pq.unit
|
277
|
+
|
278
|
+
# Return whether the supplied scalar or patient API hash value is within this range
|
279
|
+
match: (scalarOrHash) ->
|
280
|
+
val = fieldOrContainerValue(scalarOrHash, 'scalar')
|
281
|
+
(!@low_pq? || @low_pq.lessThan(val)) && (!@high_pq? || @high_pq.greaterThan(val))
|
282
|
+
@IVL_PQ = IVL_PQ
|
283
|
+
|
284
|
+
# Represents an HL7 time interval
|
285
|
+
class IVL_TS
|
286
|
+
constructor: (@low, @high) ->
|
287
|
+
|
288
|
+
# add an offset to the upper and lower bounds
|
289
|
+
add: (pq) ->
|
290
|
+
if @low
|
291
|
+
@low.add(pq)
|
292
|
+
if @high
|
293
|
+
@high.add(pq)
|
294
|
+
this
|
295
|
+
|
296
|
+
# During: this low is after other low and this high is before other high
|
297
|
+
DURING: (other) -> this.SDU(other) && this.EDU(other)
|
298
|
+
|
299
|
+
# Overlap: this overlaps with other
|
300
|
+
OVERLAP: (other) -> this.SDU(other) || this.EDU(other) || (this.SBS(other) && this.EAE(other))
|
301
|
+
|
302
|
+
# Concurrent: this low and high are the same as other low and high
|
303
|
+
CONCURRENT: (other) -> this.SCW(other) && this.ECW(other)
|
304
|
+
|
305
|
+
# Starts Before Start: this low is before other low
|
306
|
+
SBS: (other) ->
|
307
|
+
if @low && other.low
|
308
|
+
@low.before(other.low)
|
309
|
+
else
|
310
|
+
false
|
311
|
+
|
312
|
+
# Starts After Start: this low is after other low
|
313
|
+
SAS: (other) ->
|
314
|
+
if @low && other.low
|
315
|
+
@low.after(other.low)
|
316
|
+
else
|
317
|
+
false
|
318
|
+
|
319
|
+
# Starts Before End: this low is before other high
|
320
|
+
SBE: (other) ->
|
321
|
+
if @low && other.high
|
322
|
+
@low.before(other.high)
|
323
|
+
else
|
324
|
+
false
|
325
|
+
|
326
|
+
# Starts After End: this low is after other high
|
327
|
+
SAE: (other) ->
|
328
|
+
if @low && other.high
|
329
|
+
@low.after(other.high)
|
330
|
+
else
|
331
|
+
false
|
332
|
+
|
333
|
+
# Ends Before Start: this high is before other low
|
334
|
+
EBS: (other) ->
|
335
|
+
if @high && other.low
|
336
|
+
@high.before(other.low)
|
337
|
+
else
|
338
|
+
false
|
339
|
+
|
340
|
+
# Ends After Start: this high is after other low
|
341
|
+
EAS: (other) ->
|
342
|
+
if @high && other.low
|
343
|
+
@high.after(other.low)
|
344
|
+
else
|
345
|
+
false
|
346
|
+
|
347
|
+
# Ends Before End: this high is before other high
|
348
|
+
EBE: (other) ->
|
349
|
+
if @high && other.high
|
350
|
+
@high.before(other.high)
|
351
|
+
else
|
352
|
+
false
|
353
|
+
|
354
|
+
# Ends After End: this high is after other high
|
355
|
+
EAE: (other) ->
|
356
|
+
if @high && other.high
|
357
|
+
@high.after(other.high)
|
358
|
+
else
|
359
|
+
false
|
360
|
+
|
361
|
+
# Starts During: this low is between other low and high
|
362
|
+
SDU: (other) ->
|
363
|
+
if @low && other.low && other.high
|
364
|
+
@low.afterOrConcurrent(other.low) && @low.beforeOrConcurrent(other.high)
|
365
|
+
else
|
366
|
+
false
|
367
|
+
|
368
|
+
# Ends During: this high is between other low and high
|
369
|
+
EDU: (other) ->
|
370
|
+
if @high && other.low && other.high
|
371
|
+
@high.afterOrConcurrent(other.low) && @high.beforeOrConcurrent(other.high)
|
372
|
+
else
|
373
|
+
false
|
374
|
+
|
375
|
+
# Ends Concurrent With: this high is the same as other high ignoring seconds
|
376
|
+
ECW: (other) ->
|
377
|
+
if @high && other.high
|
378
|
+
@high.asDate() && other.high.asDate() && @high.withinSameMinute(other.high)
|
379
|
+
else
|
380
|
+
false
|
381
|
+
|
382
|
+
# Starts Concurrent With: this low is the same as other low ignoring seconds
|
383
|
+
SCW: (other) ->
|
384
|
+
if @low && other.low
|
385
|
+
@low.asDate() && other.low.asDate() && @low.withinSameMinute(other.low)
|
386
|
+
else
|
387
|
+
false
|
388
|
+
|
389
|
+
# Ends Concurrent With Start: this high is the same as other low ignoring seconds
|
390
|
+
ECWS: (other) ->
|
391
|
+
if @high && other.low
|
392
|
+
@high.asDate() && other.low.asDate() && @high.withinSameMinute(other.low)
|
393
|
+
else
|
394
|
+
false
|
395
|
+
|
396
|
+
# Starts Concurrent With End: this low is the same as other high ignoring seconds
|
397
|
+
SCWE: (other) ->
|
398
|
+
if @low && other.high
|
399
|
+
@low.asDate() && other.high.asDate() && @low.withinSameMinute(other.high)
|
400
|
+
else
|
401
|
+
false
|
402
|
+
@IVL_TS = IVL_TS
|
403
|
+
|
404
|
+
# Used to represent a value that will match any other value that is not null.
|
405
|
+
class ANYNonNull
|
406
|
+
constructor: ->
|
407
|
+
match: (scalarOrHash) ->
|
408
|
+
val = fieldOrContainerValue(scalarOrHash, 'scalar')
|
409
|
+
val != null
|
410
|
+
@ANYNonNull = ANYNonNull
|
411
|
+
|
412
|
+
# Returns true if one or more of the supplied values is true
|
413
|
+
atLeastOneTrue = (values...) ->
|
414
|
+
trueValues = (value for value in values when value && value.isTrue())
|
415
|
+
trueValues.length>0
|
416
|
+
Specifics.unionAll(new Boolean(trueValues.length>0), values)
|
417
|
+
@atLeastOneTrue = atLeastOneTrue
|
418
|
+
|
419
|
+
# Returns true if all of the supplied values are true
|
420
|
+
allTrue = (values...) ->
|
421
|
+
trueValues = (value for value in values when value && value.isTrue())
|
422
|
+
Specifics.intersectAll(new Boolean(trueValues.length>0 && trueValues.length==values.length), values)
|
423
|
+
@allTrue = allTrue
|
424
|
+
|
425
|
+
# Returns true if one or more of the supplied values is false
|
426
|
+
atLeastOneFalse = (values...) ->
|
427
|
+
falseValues = (value for value in values when value.isFalse())
|
428
|
+
Specifics.intersectAll(new Boolean(falseValues.length>0), values, true)
|
429
|
+
@atLeastOneFalse = atLeastOneFalse
|
430
|
+
|
431
|
+
# Returns true if all of the supplied values are false
|
432
|
+
allFalse = (values...) ->
|
433
|
+
falseValues = (value for value in values when value.isFalse())
|
434
|
+
Specifics.unionAll(new Boolean(falseValues.length>0 && falseValues.length==values.length), values, true)
|
435
|
+
@allFalse = allFalse
|
436
|
+
|
437
|
+
# Return true if compareTo matches value
|
438
|
+
matchingValue = (value, compareTo) ->
|
439
|
+
new Boolean(compareTo.match(value))
|
440
|
+
@matchingValue = matchingValue
|
441
|
+
|
442
|
+
# Return true if valueToMatch matches any event value
|
443
|
+
anyMatchingValue = (event, valueToMatch) ->
|
444
|
+
matchingValues = (value for value in event.values() when (valueToMatch.match(value)))
|
445
|
+
matchingValues.length > 0
|
446
|
+
@anyMatchingValue = anyMatchingValue
|
447
|
+
|
448
|
+
# Return only those events whose value matches the supplied value
|
449
|
+
filterEventsByValue = (events, value) ->
|
450
|
+
matchingEvents = (event for event in events when (anyMatchingValue(event, value)))
|
451
|
+
matchingEvents
|
452
|
+
@filterEventsByValue = filterEventsByValue
|
453
|
+
|
454
|
+
# Return only those events with a field that matches the supplied value
|
455
|
+
filterEventsByField = (events, field, value) ->
|
456
|
+
respondingEvents = (event for event in events when event.respondTo(field))
|
457
|
+
event for event in respondingEvents when value.match(event[field]())
|
458
|
+
@filterEventsByField = filterEventsByField
|
459
|
+
|
460
|
+
# Utility method to obtain the value set for an OID
|
461
|
+
getCodes = (oid) ->
|
462
|
+
OidDictionary[oid]
|
463
|
+
@getCodes = getCodes
|
464
|
+
|
465
|
+
# Used for representing XPRODUCTs of arrays, holds both a flattened array that contains
|
466
|
+
# all the elements of the compoent arrays and the component arrays themselves
|
467
|
+
class CrossProduct extends Array
|
468
|
+
constructor: (allEventLists) ->
|
469
|
+
super()
|
470
|
+
@eventLists = []
|
471
|
+
for eventList in allEventLists
|
472
|
+
@eventLists.push eventList
|
473
|
+
for event in eventList
|
474
|
+
this.push(event)
|
475
|
+
|
476
|
+
# Create a CrossProduct of the supplied event lists.
|
477
|
+
XPRODUCT = (eventLists...) ->
|
478
|
+
Specifics.intersectAll(new CrossProduct(eventLists), eventLists)
|
479
|
+
@XPRODUCT = XPRODUCT
|
480
|
+
|
481
|
+
# Create a new list containing all the events from the supplied event lists
|
482
|
+
UNION = (eventLists...) ->
|
483
|
+
union = []
|
484
|
+
for eventList in eventLists
|
485
|
+
for event in eventList
|
486
|
+
union.push(event)
|
487
|
+
Specifics.unionAll(union, eventLists)
|
488
|
+
@UNION = UNION
|
489
|
+
|
490
|
+
# Return true if the number of events matches the supplied range
|
491
|
+
COUNT = (events, range) ->
|
492
|
+
count = events.length
|
493
|
+
result = new Boolean(range.match(count))
|
494
|
+
applySpecificOccurrenceSubset('COUNT', Specifics.maintainSpecifics(result, events), range)
|
495
|
+
@COUNT = COUNT
|
496
|
+
|
497
|
+
# Convert an hQuery.CodedEntry or JS Date into an IVL_TS
|
498
|
+
getIVL = (eventOrTimeStamp) ->
|
499
|
+
if eventOrTimeStamp.asIVL_TS
|
500
|
+
eventOrTimeStamp.asIVL_TS()
|
501
|
+
else
|
502
|
+
ts = new TS()
|
503
|
+
ts.date = eventOrTimeStamp
|
504
|
+
new IVL_TS(ts, ts)
|
505
|
+
@getIVL = getIVL
|
506
|
+
|
507
|
+
eventAccessor = {
|
508
|
+
'DURING': 'low',
|
509
|
+
'OVERLAP': 'low',
|
510
|
+
'SBS': 'low',
|
511
|
+
'SAS': 'low',
|
512
|
+
'SBE': 'low',
|
513
|
+
'SAE': 'low',
|
514
|
+
'EBS': 'high',
|
515
|
+
'EAS': 'high',
|
516
|
+
'EBE': 'high',
|
517
|
+
'EAE': 'high',
|
518
|
+
'SDU': 'low',
|
519
|
+
'EDU': 'high',
|
520
|
+
'ECW': 'high'
|
521
|
+
'SCW': 'low',
|
522
|
+
'ECWS': 'high'
|
523
|
+
'SCWE': 'low',
|
524
|
+
'CONCURRENT': 'low'
|
525
|
+
}
|
526
|
+
|
527
|
+
boundAccessor = {
|
528
|
+
'DURING': 'low',
|
529
|
+
'OVERLAP': 'low',
|
530
|
+
'SBS': 'low',
|
531
|
+
'SAS': 'low',
|
532
|
+
'SBE': 'high',
|
533
|
+
'SAE': 'high',
|
534
|
+
'EBS': 'low',
|
535
|
+
'EAS': 'low',
|
536
|
+
'EBE': 'high',
|
537
|
+
'EAE': 'high',
|
538
|
+
'SDU': 'low',
|
539
|
+
'EDU': 'low',
|
540
|
+
'ECW': 'high'
|
541
|
+
'SCW': 'low',
|
542
|
+
'ECWS': 'low'
|
543
|
+
'SCWE': 'high',
|
544
|
+
'CONCURRENT': 'low'
|
545
|
+
}
|
546
|
+
|
547
|
+
# Determine whether the supplied event falls within range of the supplied bound
|
548
|
+
# using the method to determine which property of the event and bound to use in
|
549
|
+
# the comparison. E.g. if method is SBS then check whether the start of the event
|
550
|
+
# is within range of the start of the bound.
|
551
|
+
withinRange = (method, eventIVL, boundIVL, range) ->
|
552
|
+
eventTS = eventIVL[eventAccessor[method]]
|
553
|
+
boundTS = boundIVL[boundAccessor[method]]
|
554
|
+
range.match(eventTS.difference(boundTS, range.unit()))
|
555
|
+
@withinRange = withinRange
|
556
|
+
|
557
|
+
# Determine which bounds an event matches
|
558
|
+
eventMatchesBounds = (event, bounds, methodName, range) ->
|
559
|
+
if bounds.eventLists
|
560
|
+
# XPRODUCT set of bounds - event must match at least one bound in all members
|
561
|
+
matchingBounds = []
|
562
|
+
for boundList in bounds.eventLists
|
563
|
+
currentMatches = eventMatchesBounds(event, boundList, methodName, range)
|
564
|
+
return [] if (currentMatches.length == 0)
|
565
|
+
matchingBounds = matchingBounds.concat(currentMatches)
|
566
|
+
return Specifics.maintainSpecifics(matchingBounds,bounds)
|
567
|
+
else
|
568
|
+
eventIVL = getIVL(event)
|
569
|
+
matchingBounds = (bound for bound in bounds when (
|
570
|
+
boundIVL = getIVL(bound)
|
571
|
+
result = eventIVL[methodName](boundIVL)
|
572
|
+
if result && range
|
573
|
+
result &&= withinRange(methodName, eventIVL, boundIVL, range)
|
574
|
+
result
|
575
|
+
))
|
576
|
+
Specifics.maintainSpecifics(matchingBounds, bounds)
|
577
|
+
@eventMatchesBounds = eventMatchesBounds
|
578
|
+
|
579
|
+
# Determine which event match one of the supplied bounds
|
580
|
+
eventsMatchBounds = (events, bounds, methodName, range) ->
|
581
|
+
if (bounds.length==undefined)
|
582
|
+
bounds = [bounds]
|
583
|
+
if (events.length==undefined)
|
584
|
+
events = [events]
|
585
|
+
|
586
|
+
specificContext = new Specifics()
|
587
|
+
hasSpecificOccurrence = (events.specific_occurrence? || bounds.specific_occurrence?)
|
588
|
+
matchingEvents = []
|
589
|
+
matchingEvents.specific_occurrence = events.specific_occurrence
|
590
|
+
for event in events
|
591
|
+
matchingBounds=eventMatchesBounds(event, bounds, methodName, range)
|
592
|
+
matchingEvents.push(event) if matchingBounds.length > 0
|
593
|
+
|
594
|
+
if hasSpecificOccurrence
|
595
|
+
matchingEvents.specific_occurrence = events.specific_occurrence
|
596
|
+
# TODO: we'll need a temporary variable for non specific occurrences on the left so that we can do rejections based on restrictions in the data criteria
|
597
|
+
specificContext.addRows(Row.buildRowsForMatching(events.specific_occurrence, event, bounds.specific_occurrence, matchingBounds))
|
598
|
+
else
|
599
|
+
# add all stars
|
600
|
+
specificContext.addIdentityRow()
|
601
|
+
|
602
|
+
matchingEvents.specificContext = specificContext.finalizeEvents(events.specificContext, bounds.specificContext)
|
603
|
+
|
604
|
+
matchingEvents
|
605
|
+
@eventsMatchBounds = eventsMatchBounds
|
606
|
+
|
607
|
+
DURING = (events, bounds, offset) ->
|
608
|
+
eventsMatchBounds(events, bounds, "DURING", offset)
|
609
|
+
@DURING = DURING
|
610
|
+
|
611
|
+
OVERLAP = (events, bounds, offset) ->
|
612
|
+
eventsMatchBounds(events, bounds, "OVERLAP", offset)
|
613
|
+
@OVERLAP = OVERLAP
|
614
|
+
|
615
|
+
SBS = (events, bounds, offset) ->
|
616
|
+
eventsMatchBounds(events, bounds, "SBS", offset)
|
617
|
+
@SBS = SBS
|
618
|
+
|
619
|
+
SAS = (events, bounds, offset) ->
|
620
|
+
eventsMatchBounds(events, bounds, "SAS", offset)
|
621
|
+
@SAS = SAS
|
622
|
+
|
623
|
+
SBE = (events, bounds, offset) ->
|
624
|
+
eventsMatchBounds(events, bounds, "SBE", offset)
|
625
|
+
@SBE = SBE
|
626
|
+
|
627
|
+
SAE = (events, bounds, offset) ->
|
628
|
+
eventsMatchBounds(events, bounds, "SAE", offset)
|
629
|
+
@SAE = SAE
|
630
|
+
|
631
|
+
EBS = (events, bounds, offset) ->
|
632
|
+
eventsMatchBounds(events, bounds, "EBS", offset)
|
633
|
+
@EBS = EBS
|
634
|
+
|
635
|
+
EAS = (events, bounds, offset) ->
|
636
|
+
eventsMatchBounds(events, bounds, "EAS", offset)
|
637
|
+
@EAS = EAS
|
638
|
+
|
639
|
+
EBE = (events, bounds, offset) ->
|
640
|
+
eventsMatchBounds(events, bounds, "EBE", offset)
|
641
|
+
@EBE = EBE
|
642
|
+
|
643
|
+
EAE = (events, bounds, offset) ->
|
644
|
+
eventsMatchBounds(events, bounds, "EAE", offset)
|
645
|
+
@EAE = EAE
|
646
|
+
|
647
|
+
SDU = (events, bounds, offset) ->
|
648
|
+
eventsMatchBounds(events, bounds, "SDU", offset)
|
649
|
+
@SDU = SDU
|
650
|
+
|
651
|
+
EDU = (events, bounds, offset) ->
|
652
|
+
eventsMatchBounds(events, bounds, "EDU", offset)
|
653
|
+
@EDU = EDU
|
654
|
+
|
655
|
+
ECW = (events, bounds, offset) ->
|
656
|
+
eventsMatchBounds(events, bounds, "ECW", offset)
|
657
|
+
@ECW = ECW
|
658
|
+
|
659
|
+
SCW = (events, bounds, offset) ->
|
660
|
+
eventsMatchBounds(events, bounds, "SCW", offset)
|
661
|
+
@SCW = SCW
|
662
|
+
|
663
|
+
ECWS = (events, bounds, offset) ->
|
664
|
+
eventsMatchBounds(events, bounds, "ECWS", offset)
|
665
|
+
@ECWS = ECWS
|
666
|
+
|
667
|
+
SCWE = (events, bounds, offset) ->
|
668
|
+
eventsMatchBounds(events, bounds, "SCWE", offset)
|
669
|
+
@SCWE = SCWE
|
670
|
+
|
671
|
+
CONCURRENT = (events, bounds, offset) ->
|
672
|
+
eventsMatchBounds(events, bounds, "CONCURRENT", offset)
|
673
|
+
@CONCURRENT = CONCURRENT
|
674
|
+
|
675
|
+
dateSortDescending = (a, b) ->
|
676
|
+
b.timeStamp().getTime() - a.timeStamp().getTime()
|
677
|
+
@dateSortDescending= dateSortDescending
|
678
|
+
|
679
|
+
dateSortAscending = (a, b) ->
|
680
|
+
a.timeStamp().getTime() - b.timeStamp().getTime()
|
681
|
+
@dateSortAscending = dateSortAscending
|
682
|
+
|
683
|
+
applySpecificOccurrenceSubset = (operator, result, range, calculateSpecifics) ->
|
684
|
+
# the subset operators are re-used in the specifics calculation of those operators. Checking for a specificContext
|
685
|
+
# prevents entering into an infinite loop here.
|
686
|
+
if (result.specificContext?)
|
687
|
+
if (range?)
|
688
|
+
result.specificContext = result.specificContext[operator](range)
|
689
|
+
else
|
690
|
+
result.specificContext = result.specificContext[operator]()
|
691
|
+
result
|
692
|
+
|
693
|
+
FIRST = (events) ->
|
694
|
+
result = []
|
695
|
+
result = [events.sort(dateSortAscending)[0]] if (events.length > 0)
|
696
|
+
applySpecificOccurrenceSubset('FIRST',Specifics.maintainSpecifics(result, events))
|
697
|
+
@FIRST = FIRST
|
698
|
+
|
699
|
+
SECOND = (events) ->
|
700
|
+
result = []
|
701
|
+
result = [events.sort(dateSortAscending)[1]] if (events.length > 1)
|
702
|
+
applySpecificOccurrenceSubset('SECOND',Specifics.maintainSpecifics(result, events))
|
703
|
+
@SECOND = SECOND
|
704
|
+
|
705
|
+
THIRD = (events) ->
|
706
|
+
result = []
|
707
|
+
result = [events.sort(dateSortAscending)[2]] if (events.length > 2)
|
708
|
+
applySpecificOccurrenceSubset('THIRD',Specifics.maintainSpecifics(result, events))
|
709
|
+
@THIRD = THIRD
|
710
|
+
|
711
|
+
FOURTH = (events) ->
|
712
|
+
result = []
|
713
|
+
result = [events.sort(dateSortAscending)[3]] if (events.length > 3)
|
714
|
+
applySpecificOccurrenceSubset('FOURTH',Specifics.maintainSpecifics(result, events))
|
715
|
+
@FOURTH = FOURTH
|
716
|
+
|
717
|
+
FIFTH = (events) ->
|
718
|
+
result = []
|
719
|
+
result = [events.sort(dateSortAscending)[4]] if (events.length > 4)
|
720
|
+
applySpecificOccurrenceSubset('FIFTH',Specifics.maintainSpecifics(result, events))
|
721
|
+
@FIFTH = FIFTH
|
722
|
+
|
723
|
+
RECENT = (events) ->
|
724
|
+
result = []
|
725
|
+
result = [events.sort(dateSortDescending)[0]] if (events.length > 0)
|
726
|
+
applySpecificOccurrenceSubset('RECENT',Specifics.maintainSpecifics(result, events))
|
727
|
+
@RECENT = RECENT
|
728
|
+
|
729
|
+
LAST = (events) ->
|
730
|
+
RECENT(events)
|
731
|
+
@LAST = LAST
|
732
|
+
|
733
|
+
valueSortDescending = (a, b) ->
|
734
|
+
va = vb = Infinity
|
735
|
+
if a.value
|
736
|
+
va = a.value()["scalar"]
|
737
|
+
if b.value
|
738
|
+
vb = b.value()["scalar"]
|
739
|
+
if va==vb
|
740
|
+
0
|
741
|
+
else
|
742
|
+
vb - va
|
743
|
+
@valueSortDescending = valueSortDescending
|
744
|
+
|
745
|
+
valueSortAscending = (a, b) ->
|
746
|
+
va = vb = Infinity
|
747
|
+
if a.value
|
748
|
+
va = a.value()["scalar"]
|
749
|
+
if b.value
|
750
|
+
vb = b.value()["scalar"]
|
751
|
+
if va==vb
|
752
|
+
0
|
753
|
+
else
|
754
|
+
va - vb
|
755
|
+
@valueSortAscending = valueSortAscending
|
756
|
+
|
757
|
+
MIN = (events, range) ->
|
758
|
+
minValue = Infinity
|
759
|
+
if (events.length > 0)
|
760
|
+
minValue = events.sort(valueSortAscending)[0].value()["scalar"]
|
761
|
+
result = new Boolean(range.match(minValue))
|
762
|
+
applySpecificOccurrenceSubset('MIN',Specifics.maintainSpecifics(result, events), range)
|
763
|
+
@MIN = MIN
|
764
|
+
|
765
|
+
MAX = (events, range) ->
|
766
|
+
maxValue = -Infinity
|
767
|
+
if (events.length > 0)
|
768
|
+
maxValue = events.sort(valueSortDescending)[0].value()["scalar"]
|
769
|
+
result = new Boolean(range.match(maxValue))
|
770
|
+
applySpecificOccurrenceSubset('MAX',Specifics.maintainSpecifics(result, events), range)
|
771
|
+
@MAX = MAX
|
772
|
+
|
773
|
+
@OidDictionary = {};
|
774
|
+
|
775
|
+
hqmfjs = hqmfjs||{}
|
776
|
+
@hqmfjs = @hqmfjs||{};
|