hqmf2js 1.0.0

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