app/assets/javascripts/hqmf_util.js.coffee in hqmf2js-1.3.0 vs app/assets/javascripts/hqmf_util.js.coffee in hqmf2js-1.4.0

- old
+ new

@@ -1,8 +1,8 @@ # Represents an HL7 timestamp class TS - + # Create a new TS instance # hl7ts - an HL7 TS value as a string, e.g. 20121023131023 for # Oct 23, 2012 at 13:10:23. constructor: (hl7ts, @inclusive=false) -> if hl7ts @@ -16,11 +16,11 @@ if isNaN(minute) minute = 0 @date = new Date(Date.UTC(year, month, day, hour, minute)) else @date = new Date() - + # Add a time period to th and return it # pq - a time period as an instance of PQ. Supports units of a (year), mo (month), # wk (week), d (day), h (hour) and min (minute). add: (pq) -> if pq.unit=="a" @@ -34,17 +34,17 @@ else if pq.unit=="h" @date.setUTCHours(@date.getUTCHours()+pq.value) else if pq.unit=="min" @date.setUTCMinutes(@date.getUTCMinutes()+pq.value) else - throw "Unknown time unit: "+pq.unit + throw new Error("Unknown time unit: "+pq.unit) this - + # Returns the difference between this TS and the supplied TS as an absolute # number using the supplied granularity. E.g. if granularity is specified as year # then it will return the number of years between this TS and the supplied TS. - # granularity - specifies the granularity of the difference. Supports units + # granularity - specifies the granularity of the difference. Supports units # of a (year), mo (month), wk (week), d (day), h (hour) and min (minute). difference: (ts, granularity) -> earlier = later = null if @afterOrConcurrent(ts) earlier = ts.asDate() @@ -64,18 +64,18 @@ else if granularity=="h" TS.hoursDifference(earlier,later) else if granularity=="min" TS.minutesDifference(earlier,later) else - throw "Unknown time unit: "+granularity - + throw new Error("Unknown time unit: "+granularity) + # Get the value of this TS as a JS Date asDate: -> @date - + # Returns whether this TS is before the supplied TS ignoring seconds - before: (other) -> + before: (other) -> if @date==null || other.date==null return false if other.inclusive @beforeOrConcurrent(other) else @@ -91,14 +91,14 @@ else [a,b] = TS.dropSeconds(@date, other.date) a.getTime() > b.getTime() equals: (other) -> - (@date==null && other.date==null) || (@date.getTime()==other.date.getTime()) + (@date==null && other.date==null) || (@date!=null && other.date!=null && @date.getTime()==other.date.getTime()) # Returns whether this TS is before or concurrent with the supplied TS ignoring seconds - beforeOrConcurrent: (other) -> + beforeOrConcurrent: (other) -> if @date==null || other.date==null return false [a,b] = TS.dropSeconds(@date, other.date) a.getTime() <= b.getTime() @@ -106,55 +106,55 @@ afterOrConcurrent: (other) -> if @date==null || other.date==null return false [a,b] = TS.dropSeconds(@date, other.date) a.getTime() >= b.getTime() - + # Return whether this TS and the supplied TS are within the same minute (i.e. # same timestamp when seconds are ignored) withinSameMinute: (other) -> [a,b] = TS.dropSeconds(@date, other.date) a.getTime()==b.getTime() - + # Number of whole years between the two time stamps (as Date objects) @yearsDifference: (earlier, later) -> if (later.getUTCMonth() < earlier.getUTCMonth()) later.getUTCFullYear()-earlier.getUTCFullYear()-1 else if (later.getUTCMonth() == earlier.getUTCMonth() && later.getUTCDate() >= earlier.getUTCDate()) later.getUTCFullYear()-earlier.getUTCFullYear() else if (later.getUTCMonth() == earlier.getUTCMonth() && later.getUTCDate() < earlier.getUTCDate()) later.getUTCFullYear()-earlier.getUTCFullYear()-1 else later.getUTCFullYear()-earlier.getUTCFullYear() - + # Number of whole months between the two time stamps (as Date objects) @monthsDifference: (earlier, later) -> if (later.getUTCDate() >= earlier.getUTCDate()) (later.getUTCFullYear()-earlier.getUTCFullYear())*12+later.getUTCMonth()-earlier.getUTCMonth() else (later.getUTCFullYear()-earlier.getUTCFullYear())*12+later.getUTCMonth()-earlier.getUTCMonth()-1 - + # Number of whole minutes between the two time stamps (as Date objects) @minutesDifference: (earlier, later) -> [e,l] = TS.dropSeconds(earlier,later) Math.floor(((l.getTime()-e.getTime())/1000)/60) - + # Number of whole hours between the two time stamps (as Date objects) @hoursDifference: (earlier, later) -> Math.floor(TS.minutesDifference(earlier,later)/60) - + # Number of days betweem the two time stamps (as Date objects) @daysDifference: (earlier, later) -> # have to discard time portion for day difference calculation purposes e = new Date(Date.UTC(earlier.getUTCFullYear(), earlier.getUTCMonth(), earlier.getUTCDate())) l = new Date(Date.UTC(later.getUTCFullYear(), later.getUTCMonth(), later.getUTCDate())) Math.floor(TS.hoursDifference(e,l)/24) - + # Number of whole weeks between the two time stmaps (as Date objects) @weeksDifference: (earlier, later) -> Math.floor(TS.daysDifference(earlier,later)/7) - + # Drop the seconds from the supplied timeStamps (as Date objects) # returns the new time stamps with seconds set to 0 as an array @dropSeconds: (timeStamps...) -> timeStampsNoSeconds = for timeStamp in timeStamps noSeconds = new Date(timeStamp.getTime()) @@ -171,10 +171,12 @@ if value? if typeof value[fieldName] == 'function' value[fieldName]() else if typeof value[fieldName] != 'undefined' value[fieldName] + else if value.json? && typeof value.json[fieldName] != 'undefined' + value.json[fieldName] else if defaultToValue value else null else @@ -182,11 +184,11 @@ @fieldOrContainerValue = fieldOrContainerValue # Represents an HL7 CD value class CD constructor: (@code, @system) -> - + # Returns whether the supplied code matches this one. match: (codeOrHash) -> # We might be passed a simple code value like "M" or a CodedEntry # Do our best to get a code value but only get a code system name if one is # supplied @@ -197,15 +199,15 @@ if @system && systemToMatch c1==c2 && @system==systemToMatch else c1==c2 @CD = CD - -# Represents a list of codes + +# Represents a list of codes class CodeList constructor: (@codes) -> - + # Returns whether the supplied code matches any of the contained codes match: (codeOrHash) -> # We might be passed a simple code value like "M" or a CodedEntry # Do our best to get a code value but only get a code system name if one is # supplied @@ -221,18 +223,18 @@ result = true else if c1==c2 # no code systems to match to just match codes result = true result @CodeList = CodeList - + # Represents and HL7 physical quantity class PQ constructor: (@value, @unit, @inclusive=true) -> - + # Helper method to make a PQ behave like a patient API value scalar: -> @value - + # Returns whether this is less than the supplied value lessThan: (scalarOrHash) -> val = fieldOrContainerValue(scalarOrHash, 'scalar') if @inclusive @lessThanOrEqual(val) @@ -254,159 +256,245 @@ # Returns whether this is greater than or equal to the supplied value greaterThanOrEqual: (scalarOrHash) -> val = fieldOrContainerValue(scalarOrHash, 'scalar') @value>=val - + # Returns whether this is equal to the supplied value or hash match: (scalarOrHash) -> val = fieldOrContainerValue(scalarOrHash, 'scalar') @value==val + + # Helper method to normalize the current value as a new PQ with 'min' precision + normalizeToMins: -> + TIME_UNITS = + a: 'years' + mo: 'months' + wk: 'weeks' + d: 'days' + h: 'hours' + min: 'minutes' + s: 'seconds' + TIME_UNITS_MAP = + a: 365 * 24 * 60 + mo: 30 * 24 * 60 + wk: 7 * 24 * 60 + d: 24 * 60 + h: 60 + min: 1 + s: 1/60 + return unless TIME_UNITS[@unit]? + # use minutes as the default precision + new PQ(@value * TIME_UNITS_MAP[@unit], 'min', @inclusive) @PQ = PQ - + # Represents an HL7 interval class IVL_PQ # Create a new instance, must supply either a lower or upper bound and if both # are supplied the units must match. constructor: (@low_pq, @high_pq) -> if !@low_pq && !@high_pq - throw "Must have a lower or upper bound" + throw new Error("Must have a lower or upper bound") if @low_pq && @low_pq.unit && @high_pq && @high_pq.unit && @low_pq.unit != @high_pq.unit - throw "Mismatched low and high units: "+@low_pq.unit+", "+@high_pq.unit + throw new Error("Mismatched low and high units: "+@low_pq.unit+", "+@high_pq.unit) unit: -> if @low_pq @low_pq.unit else @high_pq.unit - + # Return whether the supplied scalar or patient API hash value is within this range match: (scalarOrHash) -> val = fieldOrContainerValue(scalarOrHash, 'scalar') - (!@low_pq? || @low_pq.lessThan(val)) && (!@high_pq? || @high_pq.greaterThan(val)) + #Add a check for ANYNonNull value for Reference Range High And Low (Lab Test). QDM 4.2 update. + if @low_pq? && @low_pq.constructor == ANYNonNull + val != null + else if @high_pq? && @high_pq.constructor == ANYNonNull + val != null + else + (!@low_pq? || @low_pq.lessThan(val)) && (!@high_pq? || @high_pq.greaterThan(val)) + + # Helper method to normalize the current values as a new IVL_PQ with 'min' precision + normalizeToMins: -> new IVL_PQ(@low_pq?.normalizeToMins(), @high_pq?.normalizeToMins()) @IVL_PQ = IVL_PQ - + # Represents an HL7 time interval class IVL_TS constructor: (@low, @high) -> - + + # support comparison to another Date for static dates + match: (other) -> + return false unless other? + other = getTS(other, @low?.inclusive || @high?.inclusive) + if @low && @low.inclusive && @high && @high.inclusive + @low.equals(other) && @high.equals(other) + else if @low + @low.before(other) + else if @high + @high.after(other) + # add an offset to the upper and lower bounds add: (pq) -> if @low @low.add(pq) if @high @high.add(pq) this - + # During: this low is after other low and this high is before other high DURING: (other) -> this.SDU(other) && this.EDU(other) - + # Overlap: this overlaps with other - OVERLAP: (other) -> this.SDU(other) || this.EDU(other) || (this.SBS(other) && this.EAE(other)) - + OVERLAP: (other) -> + if @high.date == null && other.high.date == null + true # If neither have ends, they inherently overlap on the timeline + else if @high.date == null + !this.SAE(other) + else if other.high.date == null + !this.EBS(other) + else + this.SDU(other) || this.EDU(other) || (this.SBS(other) && this.EAE(other)) + # Concurrent: this low and high are the same as other low and high CONCURRENT: (other) -> this.SCW(other) && this.ECW(other) - + # Starts Before Start: this low is before other low - SBS: (other) -> + SBS: (other) -> if @low && other.low @low.before(other.low) else false - + # Starts After Start: this low is after other low - SAS: (other) -> + SAS: (other) -> if @low && other.low @low.after(other.low) else false - + # Starts Before End: this low is before other high SBE: (other) -> if @low && other.high @low.before(other.high) else false - + # Starts After End: this low is after other high - SAE: (other) -> + SAE: (other) -> if @low && other.high @low.after(other.high) else false - + + # Starts During: this low is between other low and high + SDU: (other) -> + if @low && other.low && other.high + @low.afterOrConcurrent(other.low) && @low.beforeOrConcurrent(other.high) + else + false + + #starts before or during: this low is less than the other low or the other high. + #if other does not have a high or does not have a low this will return false + SBDU: (other) -> + this.SBS(other) || this.SDU(other) + + # Starts Concurrent With: this low is the same as other low ignoring seconds + SCW: (other) -> + if @low && other.low + @low.asDate() && other.low.asDate() && @low.withinSameMinute(other.low) + else + false + + # Starts Concurrent With End: this low is the same as other high ignoring seconds + SCWE: (other) -> + if @low && other.high + @low.asDate() && other.high.asDate() && @low.withinSameMinute(other.high) + else + false + + #Starts Before or Concurrent with: this low is <= other low + SBCW: (other) -> + this.SBS(other) || this.SCW(other) + + SBCWE: (other) -> + this.SBE(other) || this.SCWE(other) + # Starts After or Concurrent with other: this low is >= other low + SACW: (other) -> + this.SAS(other) || this.SCW(other) + + # Starts After or Concurrent with End : This low is >= other high + SACWE: (other) -> + this.SAE(other) || this.SCWE(other) + + # Ends Before Start: this high is before other low EBS: (other) -> if @high && other.low @high.before(other.low) else false - + # Ends After Start: this high is after other low - EAS: (other) -> + EAS: (other) -> if @high && other.low @high.after(other.low) else false - + # Ends Before End: this high is before other high - EBE: (other) -> + EBE: (other) -> if @high && other.high @high.before(other.high) else false - + # Ends After End: this high is after other high EAE: (other) -> if @high && other.high @high.after(other.high) else false - - # Starts During: this low is between other low and high - SDU: (other) -> - if @low && other.low && other.high - @low.afterOrConcurrent(other.low) && @low.beforeOrConcurrent(other.high) - else - false - + # Ends During: this high is between other low and high - EDU: (other) -> + EDU: (other) -> if @high && other.low && other.high @high.afterOrConcurrent(other.low) && @high.beforeOrConcurrent(other.high) else false - + # Ends Concurrent With: this high is the same as other high ignoring seconds - ECW: (other) -> + ECW: (other) -> if @high && other.high @high.asDate() && other.high.asDate() && @high.withinSameMinute(other.high) else false - - # Starts Concurrent With: this low is the same as other low ignoring seconds - SCW: (other) -> - if @low && other.low - @low.asDate() && other.low.asDate() && @low.withinSameMinute(other.low) - else - false - + # Ends Concurrent With Start: this high is the same as other low ignoring seconds ECWS: (other) -> if @high && other.low @high.asDate() && other.low.asDate() && @high.withinSameMinute(other.low) else false - - # Starts Concurrent With End: this low is the same as other high ignoring seconds - SCWE: (other) -> - if @low && other.high - @low.asDate() && other.high.asDate() && @low.withinSameMinute(other.high) - else - false + EBDU: (other) -> + this.EBS(other) || this.EDU(other) + + EBCW: (other) -> + this.EBE(other) || this.ECW(other) + + EACW: (other) -> + this.EAE(other) || this.ECW(other) + + EBCWS: (other) -> + this.EBS(other) || this.ECWS(other) + + EACWS: (other) -> + this.EAS(other) || this.ECWS(other) + equals: (other) -> - (@low==null && other.low==null) || (@low.equals(other.low)) && (@high==null && other.high==null) || (@high.equals(other.high)) + ((@low == null && other.low == null) || (@low != null && @low.equals(other.low))) && + ((@high == null && other.high == null) || (@high != null && @high.equals(other.high))) @IVL_TS = IVL_TS # Used to represent a value that will match any other value that is not null. class ANYNonNull @@ -430,11 +518,11 @@ @evalUnlessShortCircuit = evalUnlessShortCircuit invokeAll = (patient, initialSpecificContext, fns) -> (invokeOne(patient, initialSpecificContext, fn) for fn in fns) @invokeAll = invokeAll - + # Returns true if one or more of the supplied values is true atLeastOneTrue = (precondition, patient, initialSpecificContext, valueFns...) -> evalUnlessShortCircuit -> values = invokeAll(patient, initialSpecificContext, valueFns) trueValues = (value for value in values when value && value.isTrue()) @@ -445,11 +533,11 @@ allTrue = (precondition, patient, initialSpecificContext, valueFns...) -> evalUnlessShortCircuit -> values = [] for valueFn in valueFns value = invokeOne(patient, initialSpecificContext, valueFn) - # break if the we have a false value and we're short circuiting. + # break if the we have a false value and we're short circuiting. #If we're not short circuiting then we want to calculate everything break if value.isFalse() && Logger.short_circuit values.push(value) trueValues = (value for value in values when value && value.isTrue()) if trueValues.length==valueFns.length @@ -464,11 +552,11 @@ else hqmf.SpecificsManager.intersectAll(new Boolean(false), values) @allTrue = allTrue - + # Returns true if one or more of the supplied values is false atLeastOneFalse = (precondition, patient, initialSpecificContext, valueFns...) -> # values = invokeAll(patient, initialSpecificContext, valueFns) # falseValues = (value for value in values when value.isFalse()) # hqmf.SpecificsManager.intersectAll(new Boolean(falseValues.length>0), values, true) @@ -481,19 +569,19 @@ if value.isFalse() hasFalse = true break if Logger.short_circuit hqmf.SpecificsManager.intersectAll(new Boolean(values.length>0 && hasFalse), values, true) @atLeastOneFalse = atLeastOneFalse - + # Returns true if all of the supplied values are false allFalse = (precondition, patient, initialSpecificContext, valueFns...) -> evalUnlessShortCircuit -> values = invokeAll(patient, initialSpecificContext, valueFns) falseValues = (value for value in values when value.isFalse()) hqmf.SpecificsManager.unionAll(new Boolean(falseValues.length>0 && falseValues.length==values.length), values, true) @allFalse = allFalse - + # Return true if compareTo matches value matchingValue = (value, compareTo) -> new Boolean(compareTo.match(value)) @matchingValue = matchingValue @@ -511,14 +599,46 @@ # Return only those events with a field that matches the supplied value filterEventsByField = (events, field, value) -> respondingEvents = (event for event in events when event.respondTo(field)) unit = value.unit() if value.unit? - result = (event for event in respondingEvents when value.match(event[field](unit))) + result = [] + for event in respondingEvents + # If the responding event's field has multiple attributes, check each one + if event[field](unit) instanceof Array + for attr in event[field](unit) + if value.match(attr) + result.push event + break + else + result.push event if value.match(event[field](unit)) hqmf.SpecificsManager.maintainSpecifics(result, events) @filterEventsByField = filterEventsByField +#Function that grabs events with the correct Communication Direction +filterEventsByCommunicationDirection = (events, value) -> + matchingEvents = (event for event in events when (event.json.direction == value)) + hqmf.SpecificsManager.maintainSpecifics(matchingEvents, events) +@filterEventsByCommunicationDirection = filterEventsByCommunicationDirection + +# This turns out to work similarly to eventsMatchBounds +filterEventsByReference = (events, type, possibles) -> + specificContext = new hqmf.SpecificOccurrence() + matching = [] + matching.specific_occurrence = events.specific_occrrence + for event in events when event.respondTo("references") + referencedIds = (item.referenced_id().valueOf() for item in event.referencesByType(type)) + matchingPossibles = (possible for possible in possibles when possible.id.valueOf() in referencedIds) + matching.push(event) if matchingPossibles.length > 0 + if events.specific_occurrence? || possibles.specific_occurrence? + specificContext.addRows(Row.buildRowsForMatching(events.specific_occurrence, event, possibles.specific_occurrence, matchingPossibles)) + else + specificContext.addIdentityRow() + matching.specificContext = specificContext.finalizeEvents(events.specificContext, possibles.specificContext) + matching +@filterEventsByReference = filterEventsByReference + shiftTimes = (event, field) -> shiftedEvent = new event.constructor(event.json) shiftedEvent.setTimestamp(shiftedEvent[field]()) shiftedEvent @shiftTimes = shiftTimes @@ -556,14 +676,26 @@ denormalizedEvents = [].concat denormalizedEvents... result = adjustBoundsForField(denormalizedEvents, field) hqmf.SpecificsManager.maintainSpecifics(result, events) @denormalizeEventsByLocation = denormalizeEventsByLocation +# Creates a new set of events with one location per event. Input events with more than +# one location will be duplicated once per location and each resulting event will +# be assigned one location. Start and end times of the event will be adjusted to match the +# value of the supplied field +denormalizeEventsByTransfer = (events, field) -> + respondingEvents = (event for event in events when event.respondTo(field) and event[field]()) + denormalizedEvents = (denormalizeEvent(event) for event in respondingEvents) + denormalizedEvents = [].concat denormalizedEvents... + result = adjustBoundsForField(denormalizedEvents, 'transferTime') + hqmf.SpecificsManager.maintainSpecifics(result, events) +@denormalizeEventsByTransfer = denormalizeEventsByTransfer + # Utility method to obtain the value set for an OID getCodes = (oid) -> codes = OidDictionary[oid] - throw "value set oid could not be found: #{oid}" unless codes? + throw new Error("value set oid could not be found: #{oid}") unless codes? codes @getCodes = getCodes # Used for representing XPRODUCTs of arrays, holds both a flattened array that contains # all the elements of the compoent arrays and the component arrays themselves @@ -578,32 +710,53 @@ for event in eventList this.push(event) @specific_occurrence[event.id] = eventList.specific_occurrence if eventList.specific_occurrence listCount: -> @eventLists.length childList: (index) -> @eventLists[index] + intersect: -> + result = @childList(0) || [] + for index in [1...@listCount()] by 1 + currentIds = @childList(index).map((event) -> event.id) + result = result.filter((event) -> currentIds.indexOf(event.id) >= 0) + result # Create a CrossProduct of the supplied event lists. XPRODUCT = (eventLists...) -> - hqmf.SpecificsManager.intersectAll(new CrossProduct(eventLists), eventLists) + hqmf.SpecificsManager.intersectAll(new CrossProduct(eventLists), eventLists, false, null, considerLeftMost: true) @XPRODUCT = XPRODUCT # Create a new list containing all the events from the supplied event lists UNION = (eventLists...) -> union = [] - # keep track of the specific occurrences by encounter ID. This is used in + # keep track of the specific occurrences by encounter ID. This is used in # eventsMatchBounds (specifically in buildRowsForMatching down the _.isObject path) specific_occurrence = {} for eventList in eventLists for event in eventList if eventList.specific_occurrence - specific_occurrence[event.id] ||= [] - specific_occurrence[event.id].push eventList.specific_occurrence + # If there's already an object due to a previous UNION, merge the contents + if _.isObject(eventList.specific_occurrence) + for id, occurrences of eventList.specific_occurrence + specific_occurrence[id] ||= [] + specific_occurrence[id] = _.uniq(specific_occurrence[id].concat(occurrences)) + else + specific_occurrence[event.id] ||= [] + specific_occurrence[event.id].push eventList.specific_occurrence union.push(event) union.specific_occurrence = specific_occurrence unless _.isEmpty(specific_occurrence) hqmf.SpecificsManager.unionAll(union, eventLists) @UNION = UNION +# Create a CrossProduct of the supplied event lists. +INTERSECT = (eventLists...) -> + events = hqmf.SpecificsManager.intersectAll((new CrossProduct(eventLists)).intersect(), eventLists, false, null, considerLeftMost: true) + # If the logical evaluation of an INTERSECT excludes an event, the resulting specifics should not include + # rows that refer to that event; this fixes https://jira.oncprojectracking.org/browse/BONNIE-64 + events.specificContext = events.specificContext.filterSpecificsAgainstEvents(events) + events +@INTERSECT = INTERSECT + # Return true if the number of events matches the supplied range COUNT = (events, range) -> count = events.length result = new Boolean(range.match(count)) applySpecificOccurrenceSubset('COUNT', hqmf.SpecificsManager.maintainSpecifics(result, events), range) @@ -617,11 +770,21 @@ ts = new TS() ts.date = eventOrTimeStamp new IVL_TS(ts, ts) @getIVL = getIVL -eventAccessor = { +# Convert any JS Date into a TS +getTS = (date, inclusive=false) -> + if date.asDate + date + else + ts = new TS(null, inclusive) + ts.date = date + ts +@getTS = getTS + +eventAccessor = { 'DURING': 'low', 'OVERLAP': 'low', 'SBS': 'low', 'SAS': 'low', 'SBE': 'low', @@ -634,15 +797,25 @@ 'EDU': 'high', 'ECW': 'high' 'SCW': 'low', 'ECWS': 'high' 'SCWE': 'low', + 'SBCW': 'low', + 'SBCWE': 'low', + 'SACW': 'low', + 'SACWE': 'low', + 'SBDU': 'low', + 'EBCW': 'high', + 'EBCWS': 'high', + 'EACW': 'high', + 'EACWS': 'high', + 'EADU': 'high', 'CONCURRENT': 'low', 'DATEDIFF': 'low' } -boundAccessor = { +boundAccessor = { 'DURING': 'low', 'OVERLAP': 'low', 'SBS': 'low', 'SAS': 'low', 'SBE': 'high', @@ -655,24 +828,34 @@ 'EDU': 'low', 'ECW': 'high' 'SCW': 'low', 'ECWS': 'low' 'SCWE': 'high', + 'SBCW': 'low', + 'SBCWE': 'high', + 'SACW': 'low', + 'SACWE': 'high', + 'SBDU': 'high', + 'EBCW': 'high', + 'EBCWS': 'low', + 'EACW': 'high', + 'EACWS': 'low', + 'EADU': 'low', 'CONCURRENT': 'low', 'DATEDIFF': 'low' } - + # Determine whether the supplied event falls within range of the supplied bound # using the method to determine which property of the event and bound to use in # the comparison. E.g. if method is SBS then check whether the start of the event # is within range of the start of the bound. withinRange = (method, eventIVL, boundIVL, range) -> eventTS = eventIVL[eventAccessor[method]] boundTS = boundIVL[boundAccessor[method]] range.match(eventTS.difference(boundTS, range.unit())) @withinRange = withinRange - + # Determine which bounds an event matches eventMatchesBounds = (event, bounds, methodName, range) -> if bounds.eventLists # XPRODUCT set of bounds - event must match at least one bound in all members matchingBounds = [] @@ -690,39 +873,42 @@ result &&= withinRange(methodName, eventIVL, boundIVL, range) result )) hqmf.SpecificsManager.maintainSpecifics(matchingBounds, bounds) @eventMatchesBounds = eventMatchesBounds - + # Determine which event match one of the supplied bounds eventsMatchBounds = (events, bounds, methodName, range) -> if (bounds.length==undefined) bounds = [bounds] if (events.length==undefined) events = [events] - + specificContext = new hqmf.SpecificOccurrence() - hasSpecificOccurrence = (events.specific_occurrence? || bounds.specific_occurrence?) + # For the bounds (RHS), we check not only if the immediate RHS has specifics, but also whether anything on + # the RHS has specifics steps further removed, by checking if there's a specificContext with specifics + hasSpecificOccurrence = (events.specific_occurrence? || bounds.specific_occurrence? || bounds.specificContext?.hasSpecifics()) matchingEvents = [] matchingEvents.specific_occurrence = events.specific_occurrence for event in events + continue unless event matchingBounds=eventMatchesBounds(event, bounds, methodName, range) matchingEvents.push(event) if matchingBounds.length > 0 if hasSpecificOccurrence matchingEvents.specific_occurrence = events.specific_occurrence # we use a temporary variable for non specific occurrences on the left so that we can do rejections based on restrictions in the data criteria specificContext.addRows(Row.buildRowsForMatching(events.specific_occurrence, event, bounds.specific_occurrence, matchingBounds)) else # add all stars specificContext.addIdentityRow() - + matchingEvents.specificContext = specificContext.finalizeEvents(events.specificContext, bounds.specificContext) - + matchingEvents @eventsMatchBounds = eventsMatchBounds - + DURING = (events, bounds, offset) -> eventsMatchBounds(events, bounds, "DURING", offset) @DURING = DURING OVERLAP = (events, bounds, offset) -> @@ -770,23 +956,62 @@ @EDU = EDU ECW = (events, bounds, offset) -> eventsMatchBounds(events, bounds, "ECW", offset) @ECW = ECW - + SCW = (events, bounds, offset) -> eventsMatchBounds(events, bounds, "SCW", offset) @SCW = SCW ECWS = (events, bounds, offset) -> eventsMatchBounds(events, bounds, "ECWS", offset) @ECWS = ECWS - + SCWE = (events, bounds, offset) -> eventsMatchBounds(events, bounds, "SCWE", offset) @SCWE = SCWE +EBDU = (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "EBDU", offset) +@EBDU = EBDU + +EBCW = (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "EBCW", offset) +@EBCW = EBCW +EACW = (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "EACW", offset) +@EACW =EACW + +EBCWS = (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "EBCWS", offset) +@EBCWS = EBCWS + +EACWS = (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "EACWS", offset) +@EACWS = EACWS + +SBDU= (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "SBDU", offset) +@SBDU = SBDU + +SBCW= (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "SBCW", offset) +@SBCW = SBCW + +SBCWE= (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "SBCWE", offset) +@SBCWE = SBCWE + +SACW= (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "SACW", offset) +@SACW = SACW + +SACWE= (events, bounds, offset) -> + eventsMatchBounds(events, bounds, "SACWE", offset) +@SACWE = SACWE + CONCURRENT = (events, bounds, offset) -> eventsMatchBounds(events, bounds, "CONCURRENT", offset) @CONCURRENT = CONCURRENT dateSortDescending = (a, b) -> @@ -795,16 +1020,16 @@ dateSortAscending = (a, b) -> a.timeStamp().getTime() - b.timeStamp().getTime() @dateSortAscending = dateSortAscending -applySpecificOccurrenceSubset = (operator, result, range, calculateSpecifics) -> +applySpecificOccurrenceSubset = (operator, result, range, fields) -> # the subset operators are re-used in the specifics calculation of those operators. Checking for a specificContext # prevents entering into an infinite loop here. if (result.specificContext?) if (range?) - result.specificContext = result.specificContext[operator](range) + result.specificContext = result.specificContext[operator](range, fields) else result.specificContext = result.specificContext[operator]() result uniqueEvents = (events) -> @@ -814,59 +1039,64 @@ @uniqueEvents = uniqueEvents # if we have multiple events at the same exact time and they happen to be the one selected by FIRST, RECENT, etc # then we want to select all of these issues as the first, most recent, etc. selectConcurrent = (target, events) -> - targetIVL = target.asIVL_TS() - uniqueEvents((result for result in events when result.asIVL_TS().equals(targetIVL))) + uniqueEvents((result for result in events when target.timeStamp().getTime() == result.timeStamp().getTime())) @selectConcurrent = selectConcurrent -FIRST = (events) -> - result = [] - result = selectConcurrent(events.sort(dateSortAscending)[0], events) if (events.length > 0) - applySpecificOccurrenceSubset('FIRST',hqmf.SpecificsManager.maintainSpecifics(result, events)) +# Common code for all subset operators +applySubsetOperator = (operatorName, events, sortFunction, subsetIndex) -> + # If we have a specificContext, and there are actual specific occurrences involved (ie the specificContext + # has rows other than identity), then we have to return all the events that *might* satisfy the subset + # operator once specific occurrences are taken into account + if events.specificContext && events.specificContext.hasSpecifics() + + # Start by calculating the specific context subset, which creates at least one row for each event that + # satisfies the subset operator for at least one of the specifics + events.specificContext = events.specificContext[operatorName]() + + # Then, return only the events that can satisfy the subset operator for one or more specifics + return hqmf.SpecificsManager.filterEventsAgainstSpecifics(events) + + else + + # There's is no specific context, and that means that either there are no specifics involved or we are + # being called recursively from within the specifics handling code; in each case we just perform the + # logical operator and return the appropriate subset elements + result = [] + result = selectConcurrent(events.sort(sortFunction)[subsetIndex], events) if (events.length > subsetIndex) + hqmf.SpecificsManager.maintainSpecifics(result, events) + return result + + +FIRST = (events) -> applySubsetOperator('FIRST', events, dateSortAscending, 0) @FIRST = FIRST -SECOND = (events) -> - result = [] - result = selectConcurrent(events.sort(dateSortAscending)[1], events) if (events.length > 1) - applySpecificOccurrenceSubset('SECOND',hqmf.SpecificsManager.maintainSpecifics(result, events)) +SECOND = (events) -> applySubsetOperator('SECOND', events, dateSortAscending, 1) @SECOND = SECOND -THIRD = (events) -> - result = [] - result = selectConcurrent(events.sort(dateSortAscending)[2], events) if (events.length > 2) - applySpecificOccurrenceSubset('THIRD',hqmf.SpecificsManager.maintainSpecifics(result, events)) +THIRD = (events) -> applySubsetOperator('THIRD', events, dateSortAscending, 2) @THIRD = THIRD -FOURTH = (events) -> - result = [] - result = selectConcurrent(events.sort(dateSortAscending)[3], events) if (events.length > 3) - applySpecificOccurrenceSubset('FOURTH',hqmf.SpecificsManager.maintainSpecifics(result, events)) +FOURTH = (events) -> applySubsetOperator('FOURTH', events, dateSortAscending, 3) @FOURTH = FOURTH -FIFTH = (events) -> - result = [] - result = selectConcurrent(events.sort(dateSortAscending)[4], events) if (events.length > 4) - applySpecificOccurrenceSubset('FIFTH',hqmf.SpecificsManager.maintainSpecifics(result, events)) +FIFTH = (events) -> applySubsetOperator('FIFTH', events, dateSortAscending, 4) @FIFTH = FIFTH -RECENT = (events) -> - result = [] - result = selectConcurrent(events.sort(dateSortDescending)[0], events) if (events.length > 0) - applySpecificOccurrenceSubset('RECENT',hqmf.SpecificsManager.maintainSpecifics(result, events)) +RECENT = (events) -> applySubsetOperator('RECENT', events, dateSortDescending, 0) @RECENT = RECENT - -LAST = (events) -> - RECENT(events) + +LAST = (events) -> RECENT(events) @LAST = LAST - + valueSortDescending = (a, b) -> va = vb = Infinity if a.value va = a.value()["scalar"] - if b.value + if b.value vb = b.value()["scalar"] if va==vb 0 else vb - va @@ -874,70 +1104,238 @@ valueSortAscending = (a, b) -> va = vb = Infinity if a.value va = a.value()["scalar"] - if b.value + if b.value vb = b.value()["scalar"] if va==vb 0 else va - vb @valueSortAscending = valueSortAscending -MIN = (events, range) -> +FIELD_METHOD_UNITS = { + 'cumulativeMedicationDuration': 'd' + 'lengthOfStay': 'd' +} + +MIN = (events, range, fields) -> minValue = Infinity if (events.length > 0) minValue = events.sort(valueSortAscending)[0].value()["scalar"] result = new Boolean(range.match(minValue)) - applySpecificOccurrenceSubset('MIN',hqmf.SpecificsManager.maintainSpecifics(result, events), range) + applySpecificOccurrenceSubset('MIN',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields) @MIN = MIN -MAX = (events, range) -> +MAX = (events, range, fields) -> maxValue = -Infinity if (events.length > 0) maxValue = events.sort(valueSortDescending)[0].value()["scalar"] result = new Boolean(range.match(maxValue)) - applySpecificOccurrenceSubset('MAX',hqmf.SpecificsManager.maintainSpecifics(result, events), range) + applySpecificOccurrenceSubset('MAX',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields) @MAX = MAX +SUM = (events, range, initialSpecificContext, fields) -> + sum = 0 + comparison = range + field = fields?[0] + field = 'values' if field == 'result' + if (events.length > 0) + if field + unit = FIELD_METHOD_UNITS[field] || 'd' + if field == 'values' + sum += event[field]()['scalar'] for event in events + else + sum += event[field]() for event in events + sum = (new PQ(sum, unit, true)).normalizeToMins() + comparison = comparison.normalizeToMins() + result = new Boolean(comparison.match(sum)) + applySpecificOccurrenceSubset('SUM',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields) +@SUM = SUM + +MEDIAN = (events, range, initialSpecificContext, fields) -> + median = Infinity + comparison = range + field = fields?[0] + field = 'values' if field == 'result' + if (events.length > 0) + if field + unit = FIELD_METHOD_UNITS[field] || 'd' + if field == 'values' + values = ( event[field]()['scalar'] for event in events ) + else + values = ( event[field]() for event in events ) + sorted = _.clone(values).sort((f,s) -> f-s) + median = if sorted.length%2 then sorted[Math.floor(sorted.length/2)] else (sorted[sorted.length/2-1]+sorted[sorted.length/2]) /2 + if field != 'values' + median = (new PQ(median, unit, true)).normalizeToMins() + comparison = comparison.normalizeToMins() + result = new Boolean(comparison.match(median)) + applySpecificOccurrenceSubset('MEDIAN',hqmf.SpecificsManager.maintainSpecifics(result, events), range, fields) +@MEDIAN = MEDIAN + DATEDIFF = (events, range) -> return hqmf.SpecificsManager.maintainSpecifics(new Boolean(false), events) if events.length < 2 - throw "cannot calculate against more than 2 events" if events.length > 2 - hqmf.SpecificsManager.maintainSpecifics(new Boolean(withinRange('DATEDIFF', getIVL(events[0]), getIVL(events[1]), range)), events) + events = events.sort(dateSortAscending) + # events are now sorted, DATEDIFF is between first and last event + # throw "cannot calculate against more than 2 events" if events.length > 2 + hqmf.SpecificsManager.maintainSpecifics(new Boolean(withinRange('DATEDIFF', getIVL(events[0]), getIVL(events[events.length - 1]), range)), events) @DATEDIFF = DATEDIFF # Calculate the set of time differences in minutes between pairs of events # events - a XPRODUCT of two event lists # range - ignored # initialSpecificContext - the specific context containing one row per permissible # combination of events TIMEDIFF = (events, range, initialSpecificContext) -> if events.listCount() != 2 - throw "TIMEDIFF can only process 2 lists of events" + # handle nested events for Unions + if events.length >= 2 + event1 = events.sort(dateSortAscending)[0] + event2 = events.sort(dateSortAscending)[events.length - 1] + return [event1.asTS().difference(event2.asTS(), 'min')] + else + throw new Error("TIMEDIFF can only process 2 lists of events") eventList1 = events.childList(0) eventList2 = events.childList(1) - eventIndex1 = hqmf.SpecificsManager.getColumnIndex(eventList1.specific_occurrence) - eventIndex2 = hqmf.SpecificsManager.getColumnIndex(eventList2.specific_occurrence) - eventMap1 = {} - eventMap2 = {} - for event in eventList1 - eventMap1[event.id] = event - for event in eventList2 - eventMap2[event.id] = event - results = [] - for row in initialSpecificContext.rows - event1 = row.values[eventIndex1] - event2 = row.values[eventIndex2] - if event1 and event2 and event1 != hqmf.SpecificsManager.any and event2 != hqmf.SpecificsManager.any - # The maps contain the actual events we want to work with since these may contain - # time shifted clones of the events in the specificContext, e.g. via adjustBoundsForField - shiftedEvent1 = eventMap1[event1.id] - shiftedEvent2 = eventMap2[event2.id] - if shiftedEvent1 and shiftedEvent2 - results.push(shiftedEvent1.asTS().difference(shiftedEvent2.asTS(), 'min')) + eventIndex1 = hqmf.SpecificsManager.getColumnIndex(eventList1.specific_occurrence) if eventList1.specific_occurrence + eventIndex2 = hqmf.SpecificsManager.getColumnIndex(eventList2.specific_occurrence) if eventList2.specific_occurrence + if (eventIndex1? && eventIndex2?) + eventMap1 = {} + eventMap2 = {} + for event in eventList1 + eventMap1[event.id] = event + for event in eventList2 + eventMap2[event.id] = event + results = [] + for row in initialSpecificContext.rows + event1 = row.values[eventIndex1] + event2 = row.values[eventIndex2] + if event1 and event2 and event1 != hqmf.SpecificsManager.any and event2 != hqmf.SpecificsManager.any + # The maps contain the actual events we want to work with since these may contain + # time shifted clones of the events in the specificContext, e.g. via adjustBoundsForField + shiftedEvent1 = eventMap1[event1.id] + shiftedEvent2 = eventMap2[event2.id] + if shiftedEvent1 and shiftedEvent2 + results.push(shiftedEvent1.asTS().difference(shiftedEvent2.asTS(), 'min')) + else + if (eventList1.length > 0 && eventList2.length > 0) + event1 = eventList1.sort(dateSortAscending)[0] + event2 = eventList2.sort(dateSortAscending)[eventList2.length - 1] + results = [event1.asTS().difference(event2.asTS(), 'min')] results @TIMEDIFF = TIMEDIFF + +DATETIMEDIFF = (events, range, initialSpecificContext) -> + if range + DATEDIFF(events, range) + else + TIMEDIFF(events, range, initialSpecificContext) +@DATETIMEDIFF = DATETIMEDIFF + +#used to collect a number of days a series of date ranges may be active: Such as overlap issues with CMD +class ActiveDays + + constructor: -> + @active_days=[] + + reset: -> + @active_days=[] + + add_ivlts: (ivlts)-> + @add_range(ivlts.low,ivlts.high) + + add_range: (low,high) -> + start = @as_date(low) + end = @as_date(high) + days = (end.getTime()-start.getTime())/(1000*60*60*24) #number of days between dates + days += 1 # the above calculation only accounts for the days between and up to the end need to add the start back on + @add_days_from(start,days) + + add_days_from: (start, number_of_days) -> + for x in[0..number_of_days-1] + diff = (1000*60*60*24)*x + @add_date(new Date((start.getTime() + diff))) + + + add_date: (_date)-> + date = @as_date(_date) + formated_date = @format_date(date) + if @active_days[formated_date] + @active_days[formated_date]["count"] +=1 + else + @active_days[formated_date]={date: formated_date, count: 1} + + days_active: (low,high)-> + start = @as_date(low) + end = @as_date(high) + formated_start = @format_date(start) + formated_end = @format_date(end) + days = @active_days.slice(formated_start,formated_end+1).filter (e)-> e # the filter removes all nulls,0, and empty strings + days + + format_date: (date)-> + #format the date as an integer in the format of yyyymmdd ex 20141010 + ds = ""+date.getFullYear() + month = date.getMonth() + 1 + day = date.getDate() + ds += if month < 10 then "0"+ month else month + ds += if day < 10 then "0"+ day else day + parseInt(ds) + + date_diff: (low,high) -> + + + as_date: (date)-> + if date instanceof TS + new Date(date.asDate().getTime()) + else + new Date(date.getTime()) + + print_days_in_range: (low,high) -> + start = @as_date(low) + end = @as_date(high) + formated_start = @format_date(start) + formated_end = @format_date(end) + str = "Start :"+start.toString()+"\n" + str+= "End :"+end.toString()+"\n" + str += "Start :"+formated_start+"\n" + str+= "End :"+formated_end+"\n" + for x in @days_active(low,high) + str+="Date: "+x["date"]+ " count: "+x["count"]+"\n" + str + +@ActiveDays = ActiveDays + + +class CMD extends ActiveDays + + constructor:(@medications,@calculation_type) -> + super() + for m in @medications + @add_medication(m) + + add_medication: (medication) -> + dose = medication.dose().scalar + dosesPerDay = medication.administrationTiming().dosesPerDay() + if @calculation_type == "order" + for oi in medication.orderInformation() + totalDays = oi.quantityOrdered().value()/dose/dosesPerDay + if !isNaN(totalDays) + startDate = new Date(oi.orderDateTime()) + fills = oi.fills() || 1 + @add_days_from(startDate,totalDays*fills) + else + history = medication.fulfillmentHistory() + for fh in history + + totalDays = fh.quantityDispensed().value()/dose/dosesPerDay + if !isNaN(totalDays) + startDate = new Date(fh.dispenseDate()) + @add_days_from(startDate,totalDays) + +@CMD = CMD @OidDictionary = {}; hqmfjs = hqmfjs||{} @hqmfjs = @hqmfjs||{};