///|
pub(all) enum MeasurementUnit {
BeatsPerMinute
BreathsPerMinute
MillimetersMercury
Percent
CelsiusTenths
MilligramsPerDeciliter
MillimolesPerLiter
MicromolesPerLiter
ScorePoints
Unknown
} derive(Debug, Eq)
///|
pub(all) enum ObservationKind {
HeartRate
RespiratoryRate
SystolicBloodPressure
DiastolicBloodPressure
OxygenSaturation
Temperature
GlasgowComaScale
WhiteCellCount
PlateletCount
Bilirubin
Creatinine
Custom
} derive(Debug, Eq)
///|
pub(all) struct Observation {
kind : ObservationKind
value : Int
unit : MeasurementUnit
timestamp_minutes : Int
source : String
} derive(Debug, Eq)
///|
pub fn observation(
kind : ObservationKind,
value : Int,
unit : MeasurementUnit,
timestamp_minutes : Int,
source : String,
) -> Observation {
{ kind, value, unit, timestamp_minutes, source }
}
///|
pub fn observation_is_ordered(left : Observation, right : Observation) -> Bool {
left.timestamp_minutes <= right.timestamp_minutes
}
///|
pub fn observation_age_minutes(now : Int, item : Observation) -> Int {
if now <= item.timestamp_minutes {
0
} else {
now - item.timestamp_minutes
}
}
///|
pub fn observation_is_fresh(
now : Int,
item : Observation,
window_minutes : Int,
) -> Bool {
observation_age_minutes(now, item) <= window_minutes
}
///|
pub fn observation_delta(older : Observation, newer : Observation) -> Int {
newer.value - older.value
}
///|
pub fn observation_unit_label(unit : MeasurementUnit) -> String {
match unit {
BeatsPerMinute => "bpm"
BreathsPerMinute => "breaths/min"
MillimetersMercury => "mmHg"
Percent => "%"
CelsiusTenths => "0.1C"
MilligramsPerDeciliter => "mg/dL"
MillimolesPerLiter => "mmol/L"
MicromolesPerLiter => "umol/L"
ScorePoints => "points"
Unknown => "unknown"
}
}
///|
pub fn observation_kind_label(kind : ObservationKind) -> String {
match kind {
HeartRate => "heart_rate"
RespiratoryRate => "respiratory_rate"
SystolicBloodPressure => "systolic_bp"
DiastolicBloodPressure => "diastolic_bp"
OxygenSaturation => "oxygen_saturation"
Temperature => "temperature"
GlasgowComaScale => "gcs"
WhiteCellCount => "white_cell_count"
PlateletCount => "platelet_count"
Bilirubin => "bilirubin"
Creatinine => "creatinine"
Custom => "custom"
}
}
///|
pub(all) enum ObservationIssue {
NegativeTimestamp
EmptySource
UnitMismatch
NonFiniteRange
} derive(Debug, Eq)
///|
pub fn validate_observation(item : Observation) -> ObservationIssue? {
if item.timestamp_minutes < 0 {
Some(NegativeTimestamp)
} else if item.source.length() == 0 {
Some(EmptySource)
} else if item.unit == Unknown {
Some(UnitMismatch)
} else if item.value < -1000000 || item.value > 1000000 {
Some(NonFiniteRange)
} else {
None
}
}
///|
pub fn observation_issue_label(issue : ObservationIssue) -> String {
match issue {
NegativeTimestamp => "negative timestamp"
EmptySource => "empty source"
UnitMismatch => "unknown measurement unit"
NonFiniteRange => "measurement outside safe integer range"
}
}
///|
pub(all) struct Timeline {
observations : Array[Observation]
last_timestamp_minutes : Int
} derive(Debug, Eq)
///|
pub fn empty_timeline() -> Timeline {
{ observations: [], last_timestamp_minutes: 0 }
}
///|
pub fn timeline_length(timeline : Timeline) -> Int {
timeline.observations.length()
}
///|
pub fn timeline_add(timeline : Timeline, item : Observation) -> Timeline {
let next_timestamp = if item.timestamp_minutes >
timeline.last_timestamp_minutes {
item.timestamp_minutes
} else {
timeline.last_timestamp_minutes
}
let observations = timeline.observations.copy()
observations.push(item)
{ observations, last_timestamp_minutes: next_timestamp }
}
///|
pub fn timeline_valid(timeline : Timeline) -> Bool {
for i in 0.. Observation? {
if timeline.observations.length() == 0 {
None
} else {
Some(timeline.observations[timeline.observations.length() - 1])
}
}
///|
pub fn timeline_latest_of(
timeline : Timeline,
kind : ObservationKind,
) -> Observation? {
let mut result : Observation? = None
for item in timeline.observations {
if item.kind == kind {
result = Some(item)
}
}
result
}
///|
pub fn timeline_count_of(timeline : Timeline, kind : ObservationKind) -> Int {
let mut count = 0
for item in timeline.observations {
if item.kind == kind {
count += 1
}
}
count
}
///|
pub(all) struct Trend {
kind : ObservationKind
first_value : Int
latest_value : Int
delta : Int
samples : Int
rising : Bool
falling : Bool
stable : Bool
} derive(Debug, Eq)
///|
pub fn trend_for(timeline : Timeline, kind : ObservationKind) -> Trend? {
let mut first : Observation? = None
let mut latest : Observation? = None
let mut samples = 0
for item in timeline.observations {
if item.kind == kind {
if first is None {
first = Some(item)
}
latest = Some(item)
samples += 1
}
}
match (first, latest) {
(Some(first), Some(latest)) => {
let delta = latest.value - first.value
Some({
kind,
first_value: first.value,
latest_value: latest.value,
delta,
samples,
rising: delta > 0,
falling: delta < 0,
stable: delta == 0,
})
}
_ => None
}
}
///|
pub fn trend_direction(trend : Trend) -> String {
if trend.rising {
"rising"
} else if trend.falling {
"falling"
} else {
"stable"
}
}
///|
pub(all) struct TimelineSummary {
observations : Int
distinct_kinds : Int
invalid_observations : Int
earliest_timestamp : Int
latest_timestamp : Int
span_minutes : Int
} derive(Debug, Eq)
///|
pub fn timeline_summary(timeline : Timeline) -> TimelineSummary {
if timeline.observations.length() == 0 {
return {
observations: 0,
distinct_kinds: 0,
invalid_observations: 0,
earliest_timestamp: 0,
latest_timestamp: 0,
span_minutes: 0,
}
}
let mut invalid = 0
let mut earliest = timeline.observations[0].timestamp_minutes
let mut latest = earliest
let kinds : Array[ObservationKind] = []
for item in timeline.observations {
if validate_observation(item) is Some(_) {
invalid += 1
}
if item.timestamp_minutes < earliest {
earliest = item.timestamp_minutes
}
if item.timestamp_minutes > latest {
latest = item.timestamp_minutes
}
if !kinds.contains(item.kind) {
kinds.push(item.kind)
}
}
{
observations: timeline.observations.length(),
distinct_kinds: kinds.length(),
invalid_observations: invalid,
earliest_timestamp: earliest,
latest_timestamp: latest,
span_minutes: latest - earliest,
}
}
///|
pub(all) struct ThresholdBand {
label : String
minimum : Int
maximum : Int
points : Int
explanation : String
} derive(Debug, Eq)
///|
pub fn threshold_band(
label : String,
minimum : Int,
maximum : Int,
points : Int,
explanation : String,
) -> ThresholdBand {
{ label, minimum, maximum, points, explanation }
}
///|
pub fn threshold_contains(band : ThresholdBand, value : Int) -> Bool {
value >= band.minimum && value <= band.maximum
}
///|
pub fn threshold_find(
bands : Array[ThresholdBand],
value : Int,
) -> ThresholdBand? {
for band in bands {
if threshold_contains(band, value) {
return Some(band)
}
}
None
}
///|
pub fn threshold_points(bands : Array[ThresholdBand], value : Int) -> Int? {
match threshold_find(bands, value) {
Some(band) => Some(band.points)
None => None
}
}
///|
pub fn threshold_labels(bands : Array[ThresholdBand]) -> Array[String] {
bands.map(band => band.label)
}
///|
pub(all) struct RuleEvaluation {
rule_name : String
value : Int
matched : Bool
points : Int
band : String
explanation : String
} derive(Debug, Eq)
///|
pub fn evaluate_threshold(
rule_name : String,
value : Int,
bands : Array[ThresholdBand],
) -> RuleEvaluation {
match threshold_find(bands, value) {
Some(band) =>
{
rule_name,
value,
matched: true,
points: band.points,
band: band.label,
explanation: band.explanation,
}
None =>
{
rule_name,
value,
matched: false,
points: 0,
band: "unmatched",
explanation: "Value did not match a declared threshold band",
}
}
}
///|
pub fn sum_evaluations(evaluations : Array[RuleEvaluation]) -> Int {
evaluations.fold(init=0, (sum, item) => sum + item.points)
}
///|
pub fn matched_evaluations(
evaluations : Array[RuleEvaluation],
) -> Array[RuleEvaluation] {
evaluations.filter(item => item.matched)
}
///|
pub fn evaluation_labels(evaluations : Array[RuleEvaluation]) -> Array[String] {
evaluations.map(item => item.band)
}