kinavis_kernel/
observation.rs1use core::time::Duration;
32
33use crate::error::Result;
34use crate::time::{Instant, Utc};
35
36#[non_exhaustive]
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub enum ObservationStatus {
47 Valid,
49 Suspect,
51 Invalid,
53}
54
55impl ObservationStatus {
56 #[must_use]
59 pub const fn is_usable(self) -> bool {
60 !matches!(self, Self::Invalid)
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct Quality<U = ()> {
76 status: ObservationStatus,
77 sigma: Option<U>,
78}
79
80impl<U> Quality<U> {
81 #[must_use]
83 pub const fn new(status: ObservationStatus) -> Self {
84 Self {
85 status,
86 sigma: None,
87 }
88 }
89
90 #[must_use]
92 pub fn with_sigma(self, sigma: U) -> Self {
93 Self {
94 sigma: Some(sigma),
95 ..self
96 }
97 }
98
99 #[must_use]
101 pub const fn status(&self) -> ObservationStatus {
102 self.status
103 }
104
105 #[must_use]
107 pub const fn sigma(&self) -> Option<&U> {
108 self.sigma.as_ref()
109 }
110}
111
112impl<U> Default for Quality<U> {
113 fn default() -> Self {
116 Self::new(ObservationStatus::Valid)
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct Observed<T, U = ()> {
128 value: T,
129 taken_at: Instant<Utc>,
130 quality: Quality<U>,
131}
132
133impl<T, U> Observed<T, U> {
134 #[must_use]
136 pub const fn new(value: T, taken_at: Instant<Utc>, quality: Quality<U>) -> Self {
137 Self {
138 value,
139 taken_at,
140 quality,
141 }
142 }
143
144 #[must_use]
146 pub const fn value(&self) -> &T {
147 &self.value
148 }
149
150 #[must_use]
152 pub fn into_value(self) -> T {
153 self.value
154 }
155
156 #[must_use]
158 pub const fn taken_at(&self) -> Instant<Utc> {
159 self.taken_at
160 }
161
162 #[must_use]
164 pub const fn quality(&self) -> &Quality<U> {
165 &self.quality
166 }
167
168 pub fn age_at(&self, now: Instant<Utc>) -> Result<Duration> {
177 now.duration_since(self.taken_at)
178 }
179
180 #[must_use]
185 pub fn is_stale_at(&self, now: Instant<Utc>, limit: Duration) -> bool {
186 now.checked_duration_since(self.taken_at)
187 .is_some_and(|age| age > limit)
188 }
189
190 #[must_use]
196 pub fn map<V>(self, derive: impl FnOnce(T) -> V) -> Observed<V, U> {
197 Observed {
198 value: derive(self.value),
199 taken_at: self.taken_at,
200 quality: self.quality,
201 }
202 }
203}
204
205#[cfg(test)]
206#[allow(clippy::unwrap_used, clippy::float_cmp)]
207mod tests {
208 use super::*;
209 use crate::error::KernelError;
210 use crate::units::Speed;
211
212 fn at(seconds: i64) -> Instant<Utc> {
213 Instant::from_unix_seconds(seconds)
214 }
215
216 #[test]
217 fn age_is_computed_from_two_moments() {
218 let reading = Observed::<_, ()>::new(12.5_f64, at(100), Quality::default());
219 assert_eq!(reading.age_at(at(160)).unwrap(), Duration::from_secs(60));
220 assert_eq!(reading.age_at(at(100)).unwrap(), Duration::ZERO);
221 assert_eq!(
222 reading.age_at(at(90)),
223 Err(KernelError::TimeReversed {
224 by: Duration::from_secs(10)
225 })
226 );
227 }
228
229 #[test]
230 fn staleness_is_age_beyond_a_limit_and_never_from_the_future() {
231 let reading = Observed::<_, ()>::new((), at(100), Quality::default());
232 let limit = Duration::from_secs(30);
233 assert!(!reading.is_stale_at(at(130), limit));
234 assert!(reading.is_stale_at(at(131), limit));
235 assert!(!reading.is_stale_at(at(50), limit));
236 }
237
238 #[test]
239 fn quality_carries_a_typed_uncertainty() {
240 let sigma = Speed::from_knots(0.2).unwrap();
241 let quality = Quality::new(ObservationStatus::Suspect).with_sigma(sigma);
242 assert_eq!(quality.status(), ObservationStatus::Suspect);
243 assert_eq!(quality.sigma(), Some(&sigma));
244 assert!(quality.status().is_usable());
245 assert!(!ObservationStatus::Invalid.is_usable());
246 assert_eq!(Quality::<Speed>::default().sigma(), None);
247 }
248
249 #[test]
250 fn map_keeps_the_moment_and_the_quality() {
251 let quality =
252 Quality::new(ObservationStatus::Valid).with_sigma(Speed::from_knots(0.2).unwrap());
253 let speed = Observed::new(Speed::from_knots(10.0).unwrap(), at(100), quality);
254 let doubled = speed.map(|s| s * 2.0);
255 assert_eq!(doubled.value().knots(), 20.0);
256 assert_eq!(doubled.taken_at(), at(100));
257 assert_eq!(doubled.quality(), &quality);
258 assert_eq!(doubled.into_value().knots(), 20.0);
259 }
260
261 #[cfg(feature = "serde")]
262 #[test]
263 fn serde_round_trips() {
264 let quality =
265 Quality::new(ObservationStatus::Valid).with_sigma(Speed::from_knots(0.2).unwrap());
266 let speed = Observed::new(Speed::from_knots(10.0).unwrap(), at(100), quality);
267 let json = serde_json::to_string(&speed).unwrap();
268 assert_eq!(
269 serde_json::from_str::<Observed<Speed, Speed>>(&json).unwrap(),
270 speed
271 );
272 }
273}