Clinical Quality Framework Common FHIR Assets (US-Based)
0.1.0 - CI Build United States of America (USA)

Clinical Quality Framework Common FHIR Assets (US-Based), published by Clinical Quality Framework. This guide is not an authorized publication; it is the continuous build for version 0.1.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/cqframework/cqf-us/ and changes regularly. See the Directory of published versions

Library:

Official URL: http://fhir.org/guides/cqf/us/common/Library/CumulativeMedicationDuration Version: 0.1.0
Draft as of 2024-12-13 Computable Name: CumulativeMedicationDuration
Id: CumulativeMedicationDuration
Version: 0.1.0
Url: CumulativeMedicationDuration
Status: draft
Type:

code: logic-library

Date: 2024-12-13 15:11:39+0000
Publisher: Clinical Quality Framework
Jurisdiction: 840
Related Artifacts:

Dependencies

Parameters:
NameTypeMinMaxIn/Out
ErrorLevelstring01In
PatientResource01Out
Data Requirements:
TypeProfileMSCode Filter
Patient http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient
Content: text/cql
/* 
@description: This library provides cumulative medication duration calculation
logic for use with FHIR medication prescription, administration, and dispensing
resources. The logic here follows the guidance provided as part of the 5.6
version of Quality Data Model.
@comment: Note that the logic here assumes single-instruction dosing information.
Split-dosing, tapering, and other more complex dosing instructions are not handled.
@update:
v0.2.000
Fixed Quantity handling in duration calculations
Fixed authorDatetime null handling
Changed to provide Date-level calculation, rather than DateTime
v2.0.000
Updated as part of 2022 AU refresh
Refactored to use QICoreCommon
v2.0.001
Fixed invalid unit error evaluating RolloutIntervals
Fixed RolloutIntervals incorrectly eliminating duplicates
v3.1.000
updated to reflect changes from AU2022 QDM library changes _JKR
*/
library CumulativeMedicationDuration version '3.1.000'

using USCore version '3.1.1'
// NOTE: Including FHIR to handle MedicationDispense and MedicationAdministration (not in USCore 3.1.1)
// TODO: Remove this reference once USCore supports MedicationDispense and MedicationAdministration
using FHIR version '4.0.1'

include FHIRHelpers version '4.1.0'

// NOTE: Removed in favor of USCoreCommon
//include QICoreCommon version '1.3.000'
include USCoreCommon version '0.1.0'

// These code systems are not known to the MAT environment yet, request to have them added submitted
codesystem "V3TimingEvent": 'http://terminology.hl7.org/CodeSystem/v3-TimingEvent'
codesystem "EventTiming": 'http://hl7.org/fhir/event-timing'

code "HS": 'HS' from "V3TimingEvent" // event occurs [offset] before the hour of sleep (or trying to)
code "WAKE": 'WAKE' from "V3TimingEvent" // event occurs [offset] after waking
code "C": 'C' from "V3TimingEvent" //	event occurs at a meal (from the Latin cibus)
code "CM": 'CM' from "V3TimingEvent" //	event occurs at breakfast (from the Latin cibus matutinus)
code "CD": 'CD' from "V3TimingEvent" //	event occurs at lunch (from the Latin cibus diurnus)
code "CV": 'CV' from "V3TimingEvent" //	event occurs at dinner (from the Latin ante cibus vespertinus)
code "AC": 'AC' from "V3TimingEvent" //	event occurs [offset] before a meal (from the Latin ante cibus)
code "ACM": 'ACM' from "V3TimingEvent" //	event occurs [offset] before breakfast (from the Latin ante cibus matutinus)
code "ACD": 'ACD' from "V3TimingEvent" //	event occurs [offset] before lunch (from the Latin ante cibus diurnus)
code "ACV": 'ACV' from "V3TimingEvent" //	event occurs [offset] before dinner (from the Latin ante cibus vespertinus)
code "PC": 'PC' from "V3TimingEvent" //	event occurs [offset] after a meal (from the Latin post cibus)
code "PCM": 'PCM' from "V3TimingEvent" //	event occurs [offset] after breakfast (from the Latin post cibus matutinus)
code "PCD": 'PCD' from "V3TimingEvent" //	event occurs [offset] after lunch (from the Latin post cibus diurnus)
code "PCV": 'PCV' from "V3TimingEvent" //	event occurs [offset] after dinner (from the Latin post cibus vespertinus)

code "MORN": 'MORN' from EventTiming //	Event occurs during the morning. The exact time is unspecified and established by institution convention or patient interpretation.
code "MORN.early": 'MORN.early' from EventTiming //	Early Morning	Event occurs during the early morning. The exact time is unspecified and established by institution convention or patient interpretation.
code "MORN.late": 'MORN.late' from EventTiming // Late Morning	Event occurs during the late morning. The exact time is unspecified and established by institution convention or patient interpretation.
code "NOON": 'NOON' from EventTiming //	Noon	Event occurs around 12:00pm. The exact time is unspecified and established by institution convention or patient interpretation.
code "AFT": 'AFT' from EventTiming //	Afternoon	Event occurs during the afternoon. The exact time is unspecified and established by institution convention or patient interpretation.
code "AFT.early": 'AFT.early' from EventTiming //	Early Afternoon	Event occurs during the early afternoon. The exact time is unspecified and established by institution convention or patient interpretation.
code "AFT.late": 'AFT.late' from EventTiming //	Late Afternoon	Event occurs during the late afternoon. The exact time is unspecified and established by institution convention or patient interpretation.
code "EVE": 'EVE' from EventTiming //	Evening	Event occurs during the evening. The exact time is unspecified and established by institution convention or patient interpretation.
code "EVE.early": 'EVE.early' from EventTiming //	Early Evening	Event occurs during the early evening. The exact time is unspecified and established by institution convention or patient interpretation.
code "EVE.late": 'EVE.late' from EventTiming //	Late Evening	Event occurs during the late evening. The exact time is unspecified and established by institution convention or patient interpretation.
code "NIGHT": 'NIGHT' from EventTiming //	Night	Event occurs during the night. The exact time is unspecified and established by institution convention or patient interpretation.
code "PHS": 'PHS' from EventTiming //	After Sleep	Event occurs [offset] after subject goes to sleep. The exact time is unspecified and established by institution convention or patient interpretation.

code "Every eight hours (qualifier value)": '307469008' from USCoreCommon."SNOMEDCT" display 'Every eight hours (qualifier value)'
code "Every eight to twelve hours (qualifier value)": '396140003' from USCoreCommon."SNOMEDCT" display 'Every eight to twelve hours (qualifier value)'
code "Every forty eight hours (qualifier value)": '396131002' from USCoreCommon."SNOMEDCT" display 'Every forty eight hours (qualifier value)'
code "Every forty hours (qualifier value)": '396130001' from USCoreCommon."SNOMEDCT" display 'Every forty hours (qualifier value)'
code "Every four hours (qualifier value)": '225756002' from USCoreCommon."SNOMEDCT" display 'Every four hours (qualifier value)'
code "Every seventy two hours (qualifier value)": '396143001' from USCoreCommon."SNOMEDCT" display 'Every seventy two hours (qualifier value)'
code "Every six hours (qualifier value)": '307468000' from USCoreCommon."SNOMEDCT" display 'Every six hours (qualifier value)'
code "Every six to eight hours (qualifier value)": '396139000' from USCoreCommon."SNOMEDCT" display 'Every six to eight hours (qualifier value)'
code "Every thirty six hours (qualifier value)": '396126004' from USCoreCommon."SNOMEDCT" display 'Every thirty six hours (qualifier value)'
code "Every three to four hours (qualifier value)": '225754004' from USCoreCommon."SNOMEDCT" display 'Every three to four hours (qualifier value)'
code "Every three to six hours (qualifier value)": '396127008' from USCoreCommon."SNOMEDCT" display 'Every three to six hours (qualifier value)'
code "Every twelve hours (qualifier value)": '307470009' from USCoreCommon."SNOMEDCT" display 'Every twelve hours (qualifier value)'
code "Every twenty four hours (qualifier value)": '396125000' from USCoreCommon."SNOMEDCT" display 'Every twenty four hours (qualifier value)'
code "Every two to four hours (qualifier value)": '225752000' from USCoreCommon."SNOMEDCT" display 'Every two to four hours (qualifier value)'
code "Four times daily (qualifier value)": '307439001' from USCoreCommon."SNOMEDCT" display 'Four times daily (qualifier value)'
code "Once daily (qualifier value)": '229797004' from USCoreCommon."SNOMEDCT" display 'Once daily (qualifier value)'
code "One to four times a day (qualifier value)": '396109005' from USCoreCommon."SNOMEDCT" display 'One to four times a day (qualifier value)'
code "One to three times a day (qualifier value)": '396108002' from USCoreCommon."SNOMEDCT" display 'One to three times a day (qualifier value)'
code "One to two times a day (qualifier value)": '396107007' from USCoreCommon."SNOMEDCT" display 'One to two times a day (qualifier value)'
code "Three times daily (qualifier value)": '229798009' from USCoreCommon."SNOMEDCT" display 'Three times daily (qualifier value)'
code "Twice a day (qualifier value)": '229799001' from USCoreCommon."SNOMEDCT" display 'Twice a day (qualifier value)'
code "Two to four times a day (qualifier value)": '396111001' from USCoreCommon."SNOMEDCT" display 'Two to four times a day (qualifier value)'

parameter ErrorLevel String default 'Warning'

context Patient

/*
 Goal is to get to number of days
 Two broad approaches to the calculation:
  1) Based on supply and frequency, calculate the number of expected days the medication will cover/has covered
  2) Based on relevant period, determine a covered interval and calculate the length of that interval in days

This topic covers several use cases and illustrates how to calculate Cumulative
Medication Duration for each type of medication resource using the supply and
frequency approach.
*/

/*
  For the first approach, we need to get from frequency to a frequency/day
  So we define ToDaily
*/

/*
  Calculates daily frequency given frequency within a period
*/
define function ToDaily(frequency System.Integer, period System.Quantity):
  case period.unit
    when 'h' then frequency * (24.0 / period.value)
    when 'min' then frequency * (24.0 / period.value) * 60
    when 's' then frequency * (24.0 / period.value) * 60 * 60
    when 'd' then frequency * (24.0 / period.value) / 24
    when 'wk' then frequency * (24.0 / period.value) / (24 * 7)
    when 'mo' then frequency * (24.0 / period.value) / (24 * 30) /* assuming 30 days in month */
    when 'a' then frequency * (24.0 / period.value) / (24 * 365) /* assuming 365 days in year */
    when 'hour' then frequency * (24.0 / period.value)
    when 'minute' then frequency * (24.0 / period.value) * 60
    when 'second' then frequency * (24.0 / period.value) * 60 * 60
    when 'day' then frequency * (24.0 / period.value) / 24
    when 'week' then frequency * (24.0 / period.value) / (24 * 7)
    when 'month' then frequency * (24.0 / period.value) / (24 * 30) /* assuming 30 days in month */
    when 'year' then frequency * (24.0 / period.value) / (24 * 365) /* assuming 365 days in year */
    when 'hours' then frequency * (24.0 / period.value)
    when 'minutes' then frequency * (24.0 / period.value) * 60
    when 'seconds' then frequency * (24.0 / period.value) * 60 * 60
    when 'days' then frequency * (24.0 / period.value) / 24
    when 'weeks' then frequency * (24.0 / period.value) / (24 * 7)
    when 'months' then frequency * (24.0 / period.value) / (24 * 30) /* assuming 30 days in month */
    when 'years' then frequency * (24.0 / period.value) / (24 * 365) /* assuming 365 days in year */
    else Message(null, true, 'CMDLogic.ToDaily.UnknownUnit', ErrorLevel, 'Unknown unit ' & period.unit)
  end

/*
  Returns a daily frequency for the given code representation of frequency.
  The function expects codes from the [EventTiming](http://hl7.org/fhir/codesystem-event-timing.html) or
  [V3TimingEvent](http://hl7.org/fhir/v3/TimingEvent/cs.html) code systems.
  // TODO: Determine whether or not we should use timing.code at all....
*/
define function ToDaily(frequency Code):
  case
    when frequency ~ "HS" then 1 // event occurs [offset] before the hour of sleep (or trying to)
    when frequency ~ "WAKE" then 1 // event occurs [offset] after waking
    when frequency ~ "C" then 3 //	event occurs at a meal (from the Latin cibus)
    when frequency ~ "CM" then 1 //	event occurs at breakfast (from the Latin cibus matutinus)
    when frequency ~ "CD" then 1 //	event occurs at lunch (from the Latin cibus diurnus)
    when frequency ~ "CV" then 1 //	event occurs at dinner (from the Latin ante cibus vespertinus)
    when frequency ~ "AC" then 3 //	event occurs [offset] before a meal (from the Latin ante cibus)
    when frequency ~ "ACM" then 1 //	event occurs [offset] before breakfast (from the Latin ante cibus matutinus)
    when frequency ~ "ACD" then 1 //	event occurs [offset] before lunch (from the Latin ante cibus diurnus)
    when frequency ~ "ACV" then 1 //	event occurs [offset] before dinner (from the Latin ante cibus vespertinus)
    when frequency ~ "PC" then 3 //	event occurs [offset] after a meal (from the Latin post cibus)
    when frequency ~ "PCM" then 1 //	event occurs [offset] after breakfast (from the Latin post cibus matutinus)
    when frequency ~ "PCD" then 1 //	event occurs [offset] after lunch (from the Latin post cibus diurnus)
    when frequency ~ "PCV" then 1 //	event occurs [offset] after dinner (from the Latin post cibus vespertinus)

    when frequency ~ "MORN" then 1 //	Event occurs during the morning. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "MORN.early" then 1 //	Early Morning	Event occurs during the early morning. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "MORN.late" then 1 // Late Morning	Event occurs during the late morning. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "NOON" then 1 //	Noon	Event occurs around 12:00pm. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "AFT" then 1 //	Afternoon	Event occurs during the afternoon. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "AFT.early" then 1 //	Early Afternoon	Event occurs during the early afternoon. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "AFT.late" then 1 //	Late Afternoon	Event occurs during the late afternoon. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "EVE" then 1 //	Evening	Event occurs during the evening. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "EVE.early" then 1 //	Early Evening	Event occurs during the early evening. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "EVE.late" then 1 //	Late Evening	Event occurs during the late evening. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "NIGHT" then 1 //	Night	Event occurs during the night. The exact time is unspecified and established by institution convention or patient interpretation.
    when frequency ~ "PHS" then 1 //	After Sleep	Event occurs [offset] after subject goes to sleep. The exact time is unspecified and established by institution convention or patient interpretation.

    when frequency ~ "Once daily (qualifier value)" then 1.0
    when frequency ~ "Twice a day (qualifier value)" then 2.0
    when frequency ~ "Three times daily (qualifier value)" then 3.0
    when frequency ~ "Four times daily (qualifier value)" then 4.0
    when frequency ~ "Every twenty four hours (qualifier value)" then 1.0
    when frequency ~ "Every twelve hours (qualifier value)" then 2.0
    when frequency ~ "Every thirty six hours (qualifier value)" then 0.67
    when frequency ~ "Every eight hours (qualifier value)" then 3.0
    when frequency ~ "Every four hours (qualifier value)" then 6.0
    when frequency ~ "Every six hours (qualifier value)" then 4.0
    when frequency ~ "Every seventy two hours (qualifier value)" then 0.33
    when frequency ~ "Every forty eight hours (qualifier value)" then 0.5
    when frequency ~ "Every eight to twelve hours (qualifier value)" then 3.0
    when frequency ~ "Every six to eight hours (qualifier value)" then 4.0
    when frequency ~ "Every three to four hours (qualifier value)" then 8.0
    when frequency ~ "Every three to six hours (qualifier value)" then 8.0
    when frequency ~ "Every two to four hours (qualifier value)" then 12.0
    when frequency ~ "One to four times a day (qualifier value)" then 4.0
    when frequency ~ "One to three times a day (qualifier value)" then 3.0
    when frequency ~ "One to two times a day (qualifier value)" then 2.0
    when frequency ~ "Two to four times a day (qualifier value)" then 4.0

    else Message(null, true, 'CMDLogic.ToDaily.UnknownFrequencyCode', ErrorLevel, 'Unknown frequency code ' & frequency.code)
  end

/*
Now that we have a ToDaily function, we can approach calculation of the
duration of medication for an order. First, consider the definitions
for each element:

* 1 and only 1 dosageInstruction
* 1 and only 1 doseAndRate
* 1 timing with 1 repeat
* frequency, frequencyMax, defaulting to 1
* period, periodUnit, defaulting to 1 'd'
* doseQuantity or doseRange
* timeOfDay

* authoredOn: The date the prescription was written
* dispenseRequest.validityPeriod: Time period supply is authorized for
* dispenseRequest.quantity: amount of medication supplied per dispense
* dispenseRequest.numberOfRepeatsAllowed: number of refills authorized
* dispenseRequest.expectedSupplyDuration: number of days supply per dispense
* dosageInstruction.timing.repeat.boundsDuration: total duration of the repeat
* dosageInstruction.timing.repeat.boundsRange: range of durations of the repeat
* dosageInstruction.timing.repeat.boundsPeriod: period bounds of the repeat
* dosageInstruction.timing.repeat.count: number of times to repeat
* dosageInstruction.timing.repeat.countMax: maximum number of times to repeat
* dosageInstruction.timing.repeat.frequency: event occurs frequency times per period
* dosageInstruction.timing.repeat.frequencyMax: event occurs up to frequencyMax times per period
* dosageInstruction.timing.repeat.period: event occurs frequency times per period
* dosageInstruction.timing.repeat.periodMax: upper limit of period
* dosageInstruction.timing.repeat.periodUnit: period duration (s | min | h | d | wk | mo | a)
* dosageInstruction.timing.repeat.timeOfDay: time of day for the event (0..*)
* dosageInstruction.timing.repeat.when: event timing (HS | WAKE | C | CM | CD | CV | AC | ACM...)
* dosageInstruction.timing.code: BID | TID | QID | AM | PM | QD | QOD...
* dosageInstruction.asNeeded
* dosageInstruction.doseAndRate.doseQuantity
* dosageInstruction.doseAndRate.doseRange

If expectedSupplyDuration is present, then the duration is

    expectedSupplyDuration * (1 + numberOfRepeatsAllowed)

If expectedSupplyDuration is not present, then it must be calculated based on the quantity, dosage, and frequency:

    (quantity / (dosage * frequency)) * (1 + numberOfRepeatsAllowed)

    dosage: Coalesce(end of doseAndRate.doseRange, doseAndRate.doseQuantity)
    frequency: Coalesce(frequencyMax, frequency)
    period: Quantity(period, periodUnit)

If expectedSupplyDuration is not present and cannot be calculated, and the boundsPeriod is present (and completely specified), we can use that directly

    dosage.timing.repeat.boundsPeriod

This calculation results in a number of days, which can then be turned into a period by anchoring that to the
start of the validityPeriod or the authoredOn:

    Interval[earliestDispensable, earliestDispensable + expectedSupplyDuration - 1]

    earliestDispensable: Coalesce(start of validityPeriod, authoredOn)

The following function illustrates this completely:
*/

/*
Calculates the Medication Period for a single MedicationRequest.
MedicationRequest instances provided to this function are expected
to conform to the [MMEMedicationRequest](http://build.fhir.org/ig/cqframework/opioid-mme-r4/StructureDefinition-mmemedicationrequest.html)
profile, which expects:
* 1 and only 1 dosageInstruction, multiple dosageInstruction elements will result in an error
* 1 and only 1 doseAndRate, multiple doseAndRate elements will result in an error
* 1 timing with 1 repeat, missing timing or repeat elements will result in a null
* frequency, frequencyMax, defaulting to 1
* period, periodUnit, defaulting to 1 'd'
* timeOfDay
* doseQuantity or doseRange, missing doseQuantity and doseRange will result in a null
Note that MedicationRequest status is not considered by this calculation, as the
list of MedicationRequest instances provided to this function should already have
considered appropriate statuses, depending on the use case, typically `completed`.

NOTE: Updated return to use "date from end of boundsPeriod" to ensure result is Interval<Date>
*/
define function MedicationRequestPeriodTest(Request USCore."MedicationRequestProfile"):
  Request R
    let
      dosage: singleton from R.dosageInstruction,
      doseAndRate: singleton from dosage.doseAndRate,
      timing: dosage.timing,
      frequency: Coalesce(timing.repeat.frequencyMax, timing.repeat.frequency),
      period: Quantity(timing.repeat.period, timing.repeat.periodUnit),
      doseRange: doseAndRate.dose,
      doseQuantity: doseAndRate.dose,
      dose: Coalesce(end of doseRange, doseQuantity),
      dosesPerDay: Coalesce(ToDaily(frequency, period), Count(timing.repeat.timeOfDay), 1.0),
      boundsPeriod: timing.repeat.bounds as Interval<DateTime>,
      expectedDaysSupply: R.dispenseRequest.expectedSupplyDuration.value,
      // TODO: this isn't working as expected, convert results in null
      //daysSupply: (convert(R.dispenseRequest.expectedSupplyDuration) to days).value,
      daysSupply: (R.dispenseRequest.expectedSupplyDuration).value,
      quantity: R.dispenseRequest.quantity,
      refills: Coalesce(R.dispenseRequest.numberOfRepeatsAllowed, 0),
      startDate:
        Coalesce(
          date from start of boundsPeriod,
          date from R.authoredOn,
          date from start of R.dispenseRequest.validityPeriod
        ),
      totalDaysSupplied: Coalesce(daysSupply, quantity.value / (dose.value * dosesPerDay)) * (1 + refills)
    return { dosage: dosage, doseAndRate: doseAndRate, timing: timing, frequency: frequency, period: period,
      doseRange: doseRange, doseQuantity: doseQuantity, dose: dose, dosesPerDay: dosesPerDay, boundsPeriod: boundsPeriod,
      expectedDaysSupply: expectedDaysSupply, daysSupply: daysSupply, quantity: quantity, refills: refills, startDate: startDate, totalDaysSupplied: totalDaysSupplied,
      requestPeriod: if startDate is not null and totalDaysSupplied is not null then
        Interval[startDate, startDate + Quantity(totalDaysSupplied - 1, 'day') ]
      else if startDate is not null and boundsPeriod."high" is not null then
        Interval[startDate, date from end of boundsPeriod]
      else
        null}

define function MedicationRequestPeriod(Request USCore."MedicationRequestProfile"):
  Request R
    let
      dosage: singleton from R.dosageInstruction,
      doseAndRate: singleton from dosage.doseAndRate,
      timing: dosage.timing,
      frequency: Coalesce(timing.repeat.frequencyMax, timing.repeat.frequency),
      period: Quantity(timing.repeat.period, timing.repeat.periodUnit),
      doseRange: doseAndRate.dose,
      doseQuantity: doseAndRate.dose,
      dose: Coalesce(end of doseRange, doseQuantity),
      dosesPerDay: Coalesce(ToDaily(frequency, period), Count(timing.repeat.timeOfDay), 1.0),
      boundsPeriod: timing.repeat.bounds as Interval<DateTime>,
      // TODO: this isn't working as expected, convert results in null
      //daysSupply: (convert(R.dispenseRequest.expectedSupplyDuration) to days).value,
      daysSupply: (R.dispenseRequest.expectedSupplyDuration).value,
      quantity: R.dispenseRequest.quantity,
      refills: Coalesce(R.dispenseRequest.numberOfRepeatsAllowed, 0),
      startDate:
        Coalesce(
          date from start of boundsPeriod,
          date from R.authoredOn,
          date from start of R.dispenseRequest.validityPeriod
        ),
      totalDaysSupplied: Coalesce(daysSupply, quantity.value / (dose.value * dosesPerDay)) * (1 + refills)
    return
      if startDate is not null and totalDaysSupplied is not null then
        Interval[startDate, startDate + Quantity(totalDaysSupplied - 1, 'day') ]
      else if startDate is not null and boundsPeriod."high" is not null then
        Interval[startDate, date from end of boundsPeriod]
      else
        null

/*
Next, consider the MedicationDispense case:

* whenPrepared: When product was prepared
* whenHandedOver: When product was given out
* quantity: Amount dispensed
* daysSupply: Amount of medication expressed as a timing amount
* dosageInstruction.timing.repeat.boundsDuration: total duration of the repeat
* dosageInstruction.timing.repeat.boundsRange: range of durations of the repeat
* dosageInstruction.timing.repeat.boundsPeriod: period bounds of the repeat
* dosageInstruction.timing.repeat.count: number of times to repeat
* dosageInstruction.timing.repeat.countMax: maximum number of times to repeat
* dosageInstruction.timing.repeat.frequency: event occurs frequency times per period
* dosageInstruction.timing.repeat.frequencyMax: event occurs up to frequencyMax times per period
* dosageInstruction.timing.repeat.period: event occurs frequency times per period
* dosageInstruction.timing.repeat.periodMax: upper limit of period
* dosageInstruction.timing.repeat.periodUnit: period duration (s | min | h | d | wk | mo | a)
* dosageInstruction.timing.repeat.timeOfDay: time of day for the event (0..*)
* dosageInstruction.timing.repeat.when: event timing (HS | WAKE | C | CM | CD | CV | AC | ACM...)
* dosageInstruction.timing.code: BID | TID | QID | AM | PM | QD | QOD...
* dosageInstruction.asNeeded
* dosageInstruction.doseAndRate.doseQuantity
* dosageInstruction.doseAndRate.doseRange

We have effectively the same elements, with the same meanings, with the exception that the
event is documenting a single dispense, and does not contain refill information. In addition,
multiple dispense events would typically be present, and those would all have to be considered
as part of an overall calculation. That will be considered when we combine results, but for
this function, we'll focus on calculating the duration of a single dispense.

With a MedicationDispense, dosage information is expected to be the same as the related
MedicationRequest, so boundsPeriod would still cover the entire prescription (including refills)
and so cannot be used to calculate the interval covered by the dispense.

If the daysSupply element is present, then the duration in days is simply

    daysSupply

Note specifically that we are not considering refills, as those would be covered
by subsequent dispense records.

If daysSupplied is not present, then daysSupplied must be calculated based on
the quantity, dosage, and frequency:

    (quantity / (dosage * frequency))

This calculation results in a number of days, which can then be turned into a
period by anchoring that to the startDate, as determined by the first available
value of the start of the boundsPeriod, whenHandedOver, and whenPrepared.

   Interval[startDate, startDate + totalDaysSupplied - 1 day]
*/

/*
Calculates Medication Period for a given MedicationDispense
MedicationDispense instances provided to this function are expected
to conform to the [TODO: MMEMedicationDispense](http://build.fhir.org/ig/cqframework/opioid-mme-r4/StructureDefinition-mmemedicationdispense.html)
profile, which expects:
* 1 and only 1 dosageInstruction, multiple dosageInstruction elements will result in an error
* 1 and only 1 doseAndRate, multiple doseAndRate elements will result in an error
* 1 timing with 1 repeat, missing timing or repeat elements will result in a null
* frequency, frequencyMax, defaulting to 1
* period, periodUnit, defaulting to 1 'd'
* timeOfDay
* doseQuantity or doseRange, missing doseQuantity and doseRange will result in a null
* whenHandedOver or whenPrepared, if missing both whenHandedOver and whenPrepared will result in a null

Note that MedicationDispense status is not considered by this calculation, as the
list of MedicationDispense instances provided to this function should already have
considered appropriate statuses, depending on the use case, typically `completed`,
especially since whenHandedOver would be expected to be present for a completed
MedicationDispense.

Note also that the dosage information recorded in a dispense is expected to be a copy of the
dispense information from the related medication request. As such, the boundsPeriod would be expected
to cover the entire range, and should _not_ be used for timing of the event.

There is also active discussion in the Pharmacy WG about what date should be used if whenPrepared/whenHandedOver is not present.
This may take the form of an "original creation date", but may also be a "status change date" for dispense events that are updating
status of an existing dispense. That discussion is ongoing, so until that is resolved, this logic will return
null if whenPrepared/whenHandedOver are not available.
*/
define function MedicationDispensePeriod(Dispense "MedicationDispense"):
  Dispense D
    let
      dosage: singleton from D.dosageInstruction,
      doseAndRate: singleton from dosage.doseAndRate,
      timing: dosage.timing,
      frequency: Coalesce(timing.repeat.frequencyMax, timing.repeat.frequency),
      period: Quantity(timing.repeat.period, timing.repeat.periodUnit),
      doseRange: doseAndRate.dose,
      doseQuantity: doseAndRate.dose,
      dose: Coalesce(end of doseRange, doseQuantity),
      daysSupply: (convert D.daysSupply to days).value,
      dosesPerDay: Coalesce(ToDaily(frequency, period), Count(timing.repeat.timeOfDay), 1.0),
      startDate:
        Coalesce(
          date from D.whenHandedOver,
          date from D.whenPrepared
        ),
      totalDaysSupplied: Coalesce(daysSupply, D.quantity.value / (dose.value * dosesPerDay))
    return
      if startDate is not null and totalDaysSupplied is not null then
        Interval[startDate, startDate + Quantity(totalDaysSupplied - 1, 'day')]
      else
        null

/*
Returns the established therapeutic duration for a given medication.
This is likely measure specific, though could potentially be established for
any drug and distributed as a CodeSystem supplement.
Defaulting to 14 days here for illustration.
*/
define function TherapeuticDuration(medication Concept):
  14 days

/*
Next we consider MedicationAdministration. This data type is typically used to
capture specific administration, with the relevantPeriod capturing start and stop
time of the administration event:

* effective[x]: Start and end time of administration

However, when calculating cumulative medication duration, it is typically the
therapeutic period of the medication that should be considered. Currently neither
the Medication nor MedicationKnowledge resources provide this information, so
we model it here as a function that can potentially be implemented in a variety
of ways, including measure-specific values, as well as distribution as an RxNorm
code system supplement.

However it is obtained, if therapeutic duration can be obtained, and the effective
period has a start, the result will be

    Interval[startDate, startDate + therapeuticDuration - 1 day]

NOTE: Therapeutic duration needs to be established and until a way is implemented to
do that based on the medication, this logic is incomplete and should not be used.
*/
define function MedicationAdministrationPeriod(Administration "MedicationAdministration"):
  Administration M
    let
      therapeuticDuration: TherapeuticDuration(Administration.medication),
      startDate: date from start of Administration.effective
    return
      if startDate is not null and therapeuticDuration is not null then
        Interval[startDate, startDate + therapeuticDuration - 1]
      else
        null

/*
Now that we have functions for determining the medication period for individual
prescriptions, administrations, and dispenses, we can combine those using
an overall cumulative medication duration calculation.

There are two broad approaches to calculating cumulative duration, one that _collapses_
overlapping intervals so that calculations are not duplicated, and one that _rolls out_
overlapping intervals so that the durations are laid end-to-end.

First, we define a function that simply calculates CumulativeDuration of a set of
intervals:

NOTE: Updated argument to List<Interval<Date>> instead of List<Interval<DateTime>>
*/
define function CumulativeDuration(Intervals List<Interval<Date>>):
   if Intervals is not null then ( Sum((collapse Intervals per day)X
        return all(difference in days between start of X and 
          end of X
        )+ 1
    )
  ) 
    else null

/*
Next, we define a function that rolls out intervals:

NOTE: Updated argument to List<Interval<Date>> instead of List<Interval<DateTime>>
*/
define function RolloutIntervals(intervals List<Interval<Date>>):
 intervals I aggregate all R starting ( null as List<Interval<Date>>): R
    union ( { I X
        let S: Max({ 
          end of Last(R)+ 1 day, start of X }
        ),
        E: S + Quantity(Coalesce(duration in days of X, 0), 'day')
        return Interval[S, E]}
    )

/*
Then, we define a function that allows us to calculate based on the various medication
types:
*/

define function MedicationPeriod(
  medication Choice<USCore."MedicationRequestProfile",
    "MedicationDispense"
  >):
  case
    when medication is USCore."MedicationRequestProfile" then MedicationRequestPeriod(medication)
    when medication is MedicationDispense then MedicationDispensePeriod(medication)
    else null
  end

/*
We can then use this function, combined with the MedicationDuration functions above
to calculate Cumulative Medication Duration:

Generally speaking, we want to _roll out_ intervals from dispense and administration
events, and then collapse across that result and intervals from prescriptions.

Note also that the separation of medications by type should already be done
by this stage as well.

Calculations that combine dosages from different types of medications (such as Morphine Milligram Equivalent (MME)
or Average MME) require further consideration.
*/

define function CumulativeMedicationDuration(
  Medications List<Choice<USCore."MedicationRequestProfile",
    "MedicationDispense"
  >>):
  CumulativeDuration((
      Medications M
        where M is MedicationRequest
        return MedicationRequestPeriod(M)
    )
      union (
        RolloutIntervals(
          Medications M
            where M is MedicationDispense
            return MedicationDispensePeriod(M)
        )
      )
  )


/**********************************************************************/
/* Functions in this region are copied from opioid-mme-r4             */
/**********************************************************************/

define function Quantity(value Decimal, unit String):
  if value is not null then
    System.Quantity { value: value, unit: unit }
  else
    null
Content: application/elm+xml
Encoded data (466392 characters)
Content: application/elm+json
Encoded data (846152 characters)