use core::time::Duration;
use crate::error::Result;
use crate::time::{Instant, Utc};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ObservationStatus {
Valid,
Suspect,
Invalid,
}
impl ObservationStatus {
#[must_use]
pub const fn is_usable(self) -> bool {
!matches!(self, Self::Invalid)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quality<U = ()> {
status: ObservationStatus,
sigma: Option<U>,
}
impl<U> Quality<U> {
#[must_use]
pub const fn new(status: ObservationStatus) -> Self {
Self {
status,
sigma: None,
}
}
#[must_use]
pub fn with_sigma(self, sigma: U) -> Self {
Self {
sigma: Some(sigma),
..self
}
}
#[must_use]
pub const fn status(&self) -> ObservationStatus {
self.status
}
#[must_use]
pub const fn sigma(&self) -> Option<&U> {
self.sigma.as_ref()
}
}
impl<U> Default for Quality<U> {
fn default() -> Self {
Self::new(ObservationStatus::Valid)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Observed<T, U = ()> {
value: T,
taken_at: Instant<Utc>,
quality: Quality<U>,
}
impl<T, U> Observed<T, U> {
#[must_use]
pub const fn new(value: T, taken_at: Instant<Utc>, quality: Quality<U>) -> Self {
Self {
value,
taken_at,
quality,
}
}
#[must_use]
pub const fn value(&self) -> &T {
&self.value
}
#[must_use]
pub fn into_value(self) -> T {
self.value
}
#[must_use]
pub const fn taken_at(&self) -> Instant<Utc> {
self.taken_at
}
#[must_use]
pub const fn quality(&self) -> &Quality<U> {
&self.quality
}
pub fn age_at(&self, now: Instant<Utc>) -> Result<Duration> {
now.duration_since(self.taken_at)
}
#[must_use]
pub fn is_stale_at(&self, now: Instant<Utc>, limit: Duration) -> bool {
now.checked_duration_since(self.taken_at)
.is_some_and(|age| age > limit)
}
#[must_use]
pub fn map<V>(self, derive: impl FnOnce(T) -> V) -> Observed<V, U> {
Observed {
value: derive(self.value),
taken_at: self.taken_at,
quality: self.quality,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use crate::error::KernelError;
use crate::units::Speed;
fn at(seconds: i64) -> Instant<Utc> {
Instant::from_unix_seconds(seconds)
}
#[test]
fn age_is_computed_from_two_moments() {
let reading = Observed::<_, ()>::new(12.5_f64, at(100), Quality::default());
assert_eq!(reading.age_at(at(160)).unwrap(), Duration::from_secs(60));
assert_eq!(reading.age_at(at(100)).unwrap(), Duration::ZERO);
assert_eq!(
reading.age_at(at(90)),
Err(KernelError::TimeReversed {
by: Duration::from_secs(10)
})
);
}
#[test]
fn staleness_is_age_beyond_a_limit_and_never_from_the_future() {
let reading = Observed::<_, ()>::new((), at(100), Quality::default());
let limit = Duration::from_secs(30);
assert!(!reading.is_stale_at(at(130), limit));
assert!(reading.is_stale_at(at(131), limit));
assert!(!reading.is_stale_at(at(50), limit));
}
#[test]
fn quality_carries_a_typed_uncertainty() {
let sigma = Speed::from_knots(0.2).unwrap();
let quality = Quality::new(ObservationStatus::Suspect).with_sigma(sigma);
assert_eq!(quality.status(), ObservationStatus::Suspect);
assert_eq!(quality.sigma(), Some(&sigma));
assert!(quality.status().is_usable());
assert!(!ObservationStatus::Invalid.is_usable());
assert_eq!(Quality::<Speed>::default().sigma(), None);
}
#[test]
fn map_keeps_the_moment_and_the_quality() {
let quality =
Quality::new(ObservationStatus::Valid).with_sigma(Speed::from_knots(0.2).unwrap());
let speed = Observed::new(Speed::from_knots(10.0).unwrap(), at(100), quality);
let doubled = speed.map(|s| s * 2.0);
assert_eq!(doubled.value().knots(), 20.0);
assert_eq!(doubled.taken_at(), at(100));
assert_eq!(doubled.quality(), &quality);
assert_eq!(doubled.into_value().knots(), 20.0);
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trips() {
let quality =
Quality::new(ObservationStatus::Valid).with_sigma(Speed::from_knots(0.2).unwrap());
let speed = Observed::new(Speed::from_knots(10.0).unwrap(), at(100), quality);
let json = serde_json::to_string(&speed).unwrap();
assert_eq!(
serde_json::from_str::<Observed<Speed, Speed>>(&json).unwrap(),
speed
);
}
}