///|
fn finding(
observation : Observation,
rule : RuleKind,
flag : QualityFlag,
severity : Severity,
reason : String,
config : RuleConfig,
) -> Finding {
{
observation_id: observation.id,
rule,
flag,
severity,
reason,
original_value: observation.value,
processed_at: config.processing_time,
provenance: {
source: "moonbit-ocean-qc",
dataset: observation.platform,
observed_at: observation.timestamp,
processor: "lwq443",
version: "0.2.0",
},
}
}
///|
fn check_missing(observation : Observation, config : RuleConfig) -> Finding? {
match observation.value {
None =>
Some(
finding(observation, Missing, Missing, Error, "value is absent", config),
)
Some(_) => None
}
}
///|
fn check_range(observation : Observation, config : RuleConfig) -> Finding? {
match observation.value {
None => None
Some(value) => {
let below = match config.range_min {
Some(x) => value < x
None => false
}
let above = match config.range_max {
Some(x) => value > x
None => false
}
if below || above {
Some(
finding(
observation,
Range,
Bad,
Error,
"outside configured physical range",
config,
),
)
} else {
None
}
}
}
}
///|
fn check_climate(observation : Observation, config : RuleConfig) -> Finding? {
match observation.value {
None => None
Some(value) => {
let below = match config.climate_min {
Some(x) => value < x
None => false
}
let above = match config.climate_max {
Some(x) => value > x
None => false
}
if below || above {
Some(
finding(
observation,
ClimateAnomaly,
Suspect,
Warning,
"outside configured climate envelope",
config,
),
)
} else {
None
}
}
}
}
///|
fn check_delta(
previous : Observation,
current : Observation,
config : RuleConfig,
) -> Finding? {
match (previous.value, current.value, config.max_delta) {
(Some(a), Some(b), Some(limit)) =>
if (b - a).abs() > limit {
Some(
finding(
current,
Spike,
Suspect,
Warning,
"successive values changed too quickly",
config,
),
)
} else {
None
}
_ => None
}
}
///|
fn check_interval(
previous : Observation,
current : Observation,
config : RuleConfig,
) -> Finding? {
match config.max_interval {
Some(limit) =>
if current.timestamp - previous.timestamp > limit {
Some(
finding(
current,
Interval,
Suspect,
Warning,
"sampling interval exceeds limit",
config,
),
)
} else {
None
}
None => None
}
}
///|
fn check_spatial(
previous : Observation,
current : Observation,
config : RuleConfig,
) -> Finding? {
match config.max_speed {
None => None
Some(speed) => {
let dt = current.timestamp - previous.timestamp
if dt <= 0 {
None
} else {
let lat_delta = current.latitude - previous.latitude
let lon_delta = current.longitude - previous.longitude
let distance = (lat_delta * lat_delta + lon_delta * lon_delta).sqrt()
if distance / dt.to_double() > speed {
Some(
finding(
current,
SpatialJump,
Suspect,
Warning,
"platform moved faster than configured speed",
config,
),
)
} else {
None
}
}
}
}
}
///|
fn check_duplicate(
previous : Observation,
current : Observation,
config : RuleConfig,
) -> Finding? {
if config.duplicate_window > 0 &&
current.timestamp - previous.timestamp <= config.duplicate_window &&
previous.variable == current.variable &&
previous.value == current.value {
Some(
finding(
current,
Duplicate,
Suspect,
Warning,
"duplicate observation within time window",
config,
),
)
} else {
None
}
}