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.
Files changed (58) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +17 -0
  3. data/Gemfile +41 -0
  4. data/Gemfile.lock +202 -0
  5. data/README.md +7 -0
  6. data/Rakefile +22 -0
  7. data/VERSION +1 -0
  8. data/app/assets/javascripts/hqmf_util.js.coffee +776 -0
  9. data/app/assets/javascripts/logging_utils.js.coffee +150 -0
  10. data/app/assets/javascripts/patient_api_extension.js.coffee +36 -0
  11. data/app/assets/javascripts/specifics.js.coffee +462 -0
  12. data/bin/hqmf2js.rb +25 -0
  13. data/config/warble.rb +144 -0
  14. data/hqmf2js.gemspec +20 -0
  15. data/lib/config/codes.xml +1935 -0
  16. data/lib/generator/characteristic.js.erb +19 -0
  17. data/lib/generator/codes_to_json.rb +81 -0
  18. data/lib/generator/converter.rb +60 -0
  19. data/lib/generator/data_criteria.js.erb +47 -0
  20. data/lib/generator/derived_data.js.erb +5 -0
  21. data/lib/generator/js.rb +263 -0
  22. data/lib/generator/measure_period.js.erb +18 -0
  23. data/lib/generator/patient_data.js.erb +22 -0
  24. data/lib/generator/population_criteria.js.erb +4 -0
  25. data/lib/generator/precondition.js.erb +14 -0
  26. data/lib/hqmf2js.rb +20 -0
  27. data/lib/hquery/engine.rb +4 -0
  28. data/lib/tasks/codes.rake +12 -0
  29. data/lib/tasks/coffee.rake +15 -0
  30. data/lib/tasks/convert.rake +47 -0
  31. data/lib/tasks/cover_me.rake +8 -0
  32. data/test/fixtures/NQF59New.xml +1047 -0
  33. data/test/fixtures/codes/codes.xls +0 -0
  34. data/test/fixtures/codes/codes.xml +1941 -0
  35. data/test/fixtures/i2b2.xml +305 -0
  36. data/test/fixtures/invalid/missing_id.xml +18 -0
  37. data/test/fixtures/invalid/unknown_criteria_type.xml +16 -0
  38. data/test/fixtures/invalid/unknown_demographic_entry.xml +16 -0
  39. data/test/fixtures/invalid/unknown_population_type.xml +9 -0
  40. data/test/fixtures/invalid/unknown_value_type.xml +18 -0
  41. data/test/fixtures/js/59New.js +366 -0
  42. data/test/fixtures/js/test1.js +356 -0
  43. data/test/fixtures/js/test2.js +366 -0
  44. data/test/fixtures/json/0043.json +6 -0
  45. data/test/fixtures/json/0043_hqmf1.json +1 -0
  46. data/test/fixtures/json/0043_hqmf2.json +172 -0
  47. data/test/fixtures/json/59New.json +1352 -0
  48. data/test/fixtures/patient_api.js +2823 -0
  49. data/test/fixtures/patients/francis_drake.json +1180 -0
  50. data/test/fixtures/patients/larry_vanderman.json +645 -0
  51. data/test/test_helper.rb +58 -0
  52. data/test/unit/codes_to_json_test.rb +38 -0
  53. data/test/unit/effective_date_test.rb +48 -0
  54. data/test/unit/hqmf_from_json_javascript_test.rb +108 -0
  55. data/test/unit/hqmf_javascript_test.rb +175 -0
  56. data/test/unit/library_function_test.rb +553 -0
  57. data/test/unit/specifics_test.rb +757 -0
  58. 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||{};