Common CQL Artifacts for FHIR (US-Based)
2.0.0-ballot - Informative 2 Ballot

Common CQL Artifacts for FHIR (US-Based) - Downloaded Version null See the Directory of published versions

## Library: Cumulative Medication Duration

Official URL: http://hl7.org/fhir/us/cql/Library/CumulativeMedicationDuration | Version: 2.0.0-ballot | Highlight text changes?
Standards status: Informative Active as of 2026-04-03 | Maturity Level: 3 | Computable Name: CumulativeMedicationDuration

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.

Metadata
Title | Cumulative Medication Duration
Version | 2.0.0-ballot
Jurisdiction | United States of America
Topic | FHIR
Topic | CQL
Topic | Medication
Topic | Cumulative Medication Duration
Steward (Publisher) | HL7 International / Clinical Decision Support
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.
Type | logic-library from http://terminology.hl7.org/CodeSystem/library-type
Dependency | Description: Library FHIRHelpers Resource: http://hl7.org/fhir/uv/cql/Library/FHIRHelpers|4.0.1 Canonical URL: `http://hl7.org/fhir/uv/cql/Library/FHIRHelpers|4.0.1`
Dependency | Description: Library FHIRCommon Resource: http://hl7.org/fhir/uv/cql/Library/FHIRCommon|2.0.0 Canonical URL: `http://hl7.org/fhir/uv/cql/Library/FHIRCommon|2.0.0`
Dependency | Description: Code System V3TimingEvent Resource: TimingEvent Canonical URL: `http://terminology.hl7.org/CodeSystem/v3-TimingEvent`
Dependency | Description: Code System EventTiming Resource: EventTiming Canonical URL: `http://hl7.org/fhir/event-timing`
Guidance (Usage) | Note that the logic here assumes single-instruction dosing information. Split-dosing, tapering, and other more complex dosing instructions are not handled.
Parameters
Name | Use | Card. | Type | Documentation
ErrorLevel | In | 0..1 | string |
Patient | Out | 0..1 | Patient |
Library Content
CQL Content |
    /*
    @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.
    Note also that this logic is not US-specific, this library is a candidate for promotion to
    universal realm.
    */
    library CumulativeMedicationDuration version '2.0.0-ballot'

    using FHIR version '4.0.1'

    include hl7.fhir.uv.cql.FHIRHelpers version '4.0.1'
    include hl7.fhir.uv.cql.FHIRCommon version '2.0.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 FHIRCommon."SNOMEDCT" display 'Every eight hours (qualifier value)'
    code "Every eight to twelve hours (qualifier value)": '396140003' from FHIRCommon."SNOMEDCT" display 'Every eight to twelve hours (qualifier value)'
    code "Every forty eight hours (qualifier value)": '396131002' from FHIRCommon."SNOMEDCT" display 'Every forty eight hours (qualifier value)'
    code "Every forty hours (qualifier value)": '396130001' from FHIRCommon."SNOMEDCT" display 'Every forty hours (qualifier value)'
    code "Every four hours (qualifier value)": '225756002' from FHIRCommon."SNOMEDCT" display 'Every four hours (qualifier value)'
    code "Every seventy two hours (qualifier value)": '396143001' from FHIRCommon."SNOMEDCT" display 'Every seventy two hours (qualifier value)'
    code "Every six hours (qualifier value)": '307468000' from FHIRCommon."SNOMEDCT" display 'Every six hours (qualifier value)'
    code "Every six to eight hours (qualifier value)": '396139000' from FHIRCommon."SNOMEDCT" display 'Every six to eight hours (qualifier value)'
    code "Every thirty six hours (qualifier value)": '396126004' from FHIRCommon."SNOMEDCT" display 'Every thirty six hours (qualifier value)'
    code "Every three to four hours (qualifier value)": '225754004' from FHIRCommon."SNOMEDCT" display 'Every three to four hours (qualifier value)'
    code "Every three to six hours (qualifier value)": '396127008' from FHIRCommon."SNOMEDCT" display 'Every three to six hours (qualifier value)'
    code "Every twelve hours (qualifier value)": '307470009' from FHIRCommon."SNOMEDCT" display 'Every twelve hours (qualifier value)'
    code "Every twenty four hours (qualifier value)": '396125000' from FHIRCommon."SNOMEDCT" display 'Every twenty four hours (qualifier value)'
    code "Every two to four hours (qualifier value)": '225752000' from FHIRCommon."SNOMEDCT" display 'Every two to four hours (qualifier value)'
    code "Four times daily (qualifier value)": '307439001' from FHIRCommon."SNOMEDCT" display 'Four times daily (qualifier value)'
    code "Once daily (qualifier value)": '229797004' from FHIRCommon."SNOMEDCT" display 'Once daily (qualifier value)'
    code "One to four times a day (qualifier value)": '396109005' from FHIRCommon."SNOMEDCT" display 'One to four times a day (qualifier value)'
    code "One to three times a day (qualifier value)": '396108002' from FHIRCommon."SNOMEDCT" display 'One to three times a day (qualifier value)'
    code "One to two times a day (qualifier value)": '396107007' from FHIRCommon."SNOMEDCT" display 'One to two times a day (qualifier value)'
    code "Three times daily (qualifier value)": '229798009' from FHIRCommon."SNOMEDCT" display 'Three times daily (qualifier value)'
    code "Twice a day (qualifier value)": '229799001' from FHIRCommon."SNOMEDCT" display 'Twice a day (qualifier value)'
    code "Two to four times a day (qualifier value)": '396111001' from FHIRCommon."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.
      Note that for ranges, the boundary resulting in the highest frequency result is used.
      E.g. `Every two to four hours` results in `12.0` and `One to two` times a day results in `2.0`.
    */
    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 MedicationRequestPeriod(Request MedicationRequest):
      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 as FHIR.Range,
          doseQuantity: doseAndRate.dose as FHIR.SimpleQuantity,

          dose: Coalesce(end of doseRange, doseQuantity),
          dosesPerDay: Coalesce(ToDaily(frequency, period), Count(timing.repeat.timeOfDay), 1.0),
          boundsPeriod: timing.repeat.bounds as Period,
          // 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."end" is not null then
            Interval[startDate, date from end of boundsPeriod]
          else
            null

    /*
    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 fluent function medicationRequestPeriod(Request MedicationRequest):
      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 as FHIR.Range,
          doseQuantity: doseAndRate.dose as FHIR.SimpleQuantity,

          dose: Coalesce(end of doseRange, doseQuantity),
          dosesPerDay: Coalesce(ToDaily(frequency, period), Count(timing.repeat.timeOfDay), 1.0),
          boundsPeriod: timing.repeat.bounds as Period,
          // 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."end" 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 as FHIR.Range,
          doseQuantity: doseAndRate.dose as FHIR.SimpleQuantity,
          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

    /*
    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 fluent 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 as FHIR.Range,
          doseQuantity: doseAndRate.dose as FHIR.SimpleQuantity,
          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

    /*
    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 fluent 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

    /*
    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 fluent 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]}
        )

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

    NOTE: Updated argument to List<Interval<Date>> instead of List<Interval<DateTime>>
    */
    define fluent 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<MedicationRequest, MedicationDispense>):
      case
        when medication is MedicationRequest then MedicationRequestPeriod(medication)
        when medication is MedicationDispense then MedicationDispensePeriod(medication)
        else null
      end

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

    define fluent function medicationPeriod(medication Choice<MedicationRequest, MedicationDispense>):
      case
        when medication is MedicationRequest 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<MedicationRequest, MedicationDispense>>):
      CumulativeDuration((
          Medications M
            where M is MedicationRequest
            return MedicationRequestPeriod(M)
        )
          union (
            RolloutIntervals(
              Medications M
                where M is MedicationDispense
                return MedicationDispensePeriod(M)
            )
          )
      )

    /*
    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 fluent function cumulativeMedicationDuration(Medications List<Choice<MedicationRequest, 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
ELM XML Content |
    Encoded data
ELM JSON Content |
    Encoded data
Generated using version 0.5.3-cibuild of the sample-content-ig Liquid templates