ecad_processor/models/
temperature.rs

1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3
4use crate::error::{ProcessingError, Result};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct TemperatureRecord {
8    pub staid: u32,
9    pub souid: u32,
10    pub date: NaiveDate,
11    pub temperature: f32,
12    pub quality_flag: u8,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum QualityFlag {
17    Valid = 0,
18    Suspect = 1,
19    Missing = 9,
20}
21
22impl QualityFlag {
23    pub fn from_u8(value: u8) -> Result<Self> {
24        match value {
25            0 => Ok(QualityFlag::Valid),
26            1 => Ok(QualityFlag::Suspect),
27            9 => Ok(QualityFlag::Missing),
28            _ => Err(ProcessingError::InvalidQualityFlag(value)),
29        }
30    }
31
32    pub fn as_u8(&self) -> u8 {
33        *self as u8
34    }
35
36    pub fn as_char(&self) -> char {
37        match self {
38            QualityFlag::Valid => '0',
39            QualityFlag::Suspect => '1',
40            QualityFlag::Missing => '9',
41        }
42    }
43
44    pub fn should_enforce_strict_validation(&self) -> bool {
45        matches!(self, QualityFlag::Valid)
46    }
47
48    pub fn is_usable(&self) -> bool {
49        matches!(self, QualityFlag::Valid | QualityFlag::Suspect)
50    }
51}
52
53impl TemperatureRecord {
54    pub fn new(
55        staid: u32,
56        souid: u32,
57        date: NaiveDate,
58        temperature: f32,
59        quality_flag: u8,
60    ) -> Result<Self> {
61        QualityFlag::from_u8(quality_flag)?;
62
63        Ok(Self {
64            staid,
65            souid,
66            date,
67            temperature,
68            quality_flag,
69        })
70    }
71
72    pub fn quality(&self) -> Result<QualityFlag> {
73        QualityFlag::from_u8(self.quality_flag)
74    }
75
76    pub fn is_valid_temperature(&self) -> bool {
77        self.temperature >= -50.0 && self.temperature <= 50.0
78    }
79
80    pub fn validate(&self) -> Result<()> {
81        if !self.is_valid_temperature() {
82            return Err(ProcessingError::TemperatureValidation {
83                message: format!(
84                    "Temperature {} is outside valid range [-50, 50]",
85                    self.temperature
86                ),
87            });
88        }
89
90        Ok(())
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct TemperatureSet {
96    pub min: Option<TemperatureRecord>,
97    pub max: Option<TemperatureRecord>,
98    pub avg: Option<TemperatureRecord>,
99}
100
101impl Default for TemperatureSet {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl TemperatureSet {
108    pub fn new() -> Self {
109        Self {
110            min: None,
111            max: None,
112            avg: None,
113        }
114    }
115
116    pub fn validate_relationships(&self) -> Result<()> {
117        if let (Some(min), Some(avg), Some(max)) = (&self.min, &self.avg, &self.max) {
118            let tolerance = 0.1;
119
120            if min.temperature > avg.temperature + tolerance {
121                return Err(ProcessingError::TemperatureValidation {
122                    message: format!(
123                        "Min temperature {} > Avg temperature {}",
124                        min.temperature, avg.temperature
125                    ),
126                });
127            }
128
129            if avg.temperature > max.temperature + tolerance {
130                return Err(ProcessingError::TemperatureValidation {
131                    message: format!(
132                        "Avg temperature {} > Max temperature {}",
133                        avg.temperature, max.temperature
134                    ),
135                });
136            }
137        }
138
139        Ok(())
140    }
141
142    pub fn quality_flags_string(&self) -> String {
143        let min_flag = self
144            .min
145            .as_ref()
146            .and_then(|r| r.quality().ok())
147            .unwrap_or(QualityFlag::Missing)
148            .as_char();
149
150        let avg_flag = self
151            .avg
152            .as_ref()
153            .and_then(|r| r.quality().ok())
154            .unwrap_or(QualityFlag::Missing)
155            .as_char();
156
157        let max_flag = self
158            .max
159            .as_ref()
160            .and_then(|r| r.quality().ok())
161            .unwrap_or(QualityFlag::Missing)
162            .as_char();
163
164        format!("{}{}{}", min_flag, avg_flag, max_flag)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use chrono::NaiveDate;
172
173    #[test]
174    fn test_quality_flag_conversion() {
175        assert_eq!(QualityFlag::from_u8(0).unwrap(), QualityFlag::Valid);
176        assert_eq!(QualityFlag::from_u8(1).unwrap(), QualityFlag::Suspect);
177        assert_eq!(QualityFlag::from_u8(9).unwrap(), QualityFlag::Missing);
178        assert!(QualityFlag::from_u8(5).is_err());
179    }
180
181    #[test]
182    fn test_temperature_validation() {
183        let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
184
185        let valid = TemperatureRecord::new(1, 1, date, 25.5, 0).unwrap();
186        assert!(valid.validate().is_ok());
187
188        let invalid = TemperatureRecord::new(1, 1, date, 55.0, 0).unwrap();
189        assert!(invalid.validate().is_err());
190    }
191
192    #[test]
193    fn test_temperature_set_validation() {
194        let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
195
196        let mut temp_set = TemperatureSet::new();
197        temp_set.min = Some(TemperatureRecord::new(1, 1, date, 10.0, 0).unwrap());
198        temp_set.avg = Some(TemperatureRecord::new(1, 1, date, 15.0, 0).unwrap());
199        temp_set.max = Some(TemperatureRecord::new(1, 1, date, 20.0, 0).unwrap());
200
201        assert!(temp_set.validate_relationships().is_ok());
202        assert_eq!(temp_set.quality_flags_string(), "000");
203
204        // Invalid relationship
205        temp_set.avg = Some(TemperatureRecord::new(1, 1, date, 25.0, 0).unwrap());
206        assert!(temp_set.validate_relationships().is_err());
207    }
208}