ecad_processor/models/
consolidated.rs

1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3use validator::Validate;
4
5use crate::error::{ProcessingError, Result};
6
7#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
8pub struct ConsolidatedRecord {
9    pub station_id: u32,
10    pub station_name: String,
11    pub date: NaiveDate,
12
13    #[validate(range(min = -90.0, max = 90.0))]
14    pub latitude: f64,
15
16    #[validate(range(min = -180.0, max = 180.0))]
17    pub longitude: f64,
18
19    #[validate(range(min = -50.0, max = 50.0))]
20    pub min_temp: f32,
21
22    #[validate(range(min = -50.0, max = 50.0))]
23    pub max_temp: f32,
24
25    #[validate(range(min = -50.0, max = 50.0))]
26    pub avg_temp: f32,
27
28    pub quality_flags: String,
29}
30
31impl ConsolidatedRecord {
32    #[allow(clippy::too_many_arguments)]
33    pub fn new(
34        station_id: u32,
35        station_name: String,
36        date: NaiveDate,
37        latitude: f64,
38        longitude: f64,
39        min_temp: f32,
40        max_temp: f32,
41        avg_temp: f32,
42        quality_flags: String,
43    ) -> Self {
44        Self {
45            station_id,
46            station_name,
47            date,
48            latitude,
49            longitude,
50            min_temp,
51            max_temp,
52            avg_temp,
53            quality_flags,
54        }
55    }
56
57    pub fn validate_relationships(&self) -> Result<()> {
58        let tolerance = 1.0; // Increased tolerance for real-world data
59
60        if self.min_temp > self.avg_temp + tolerance {
61            return Err(ProcessingError::TemperatureValidation {
62                message: format!(
63                    "Min temperature {} > Avg temperature {} (tolerance={})",
64                    self.min_temp, self.avg_temp, tolerance
65                ),
66            });
67        }
68
69        if self.avg_temp > self.max_temp + tolerance {
70            return Err(ProcessingError::TemperatureValidation {
71                message: format!(
72                    "Avg temperature {} > Max temperature {} (tolerance={})",
73                    self.avg_temp, self.max_temp, tolerance
74                ),
75            });
76        }
77
78        self.validate()?;
79
80        Ok(())
81    }
82
83    pub fn temperature_range(&self) -> f32 {
84        self.max_temp - self.min_temp
85    }
86
87    pub fn has_valid_data(&self) -> bool {
88        self.quality_flags == "000"
89    }
90
91    pub fn has_suspect_data(&self) -> bool {
92        self.quality_flags.contains('1')
93    }
94
95    pub fn has_missing_data(&self) -> bool {
96        self.quality_flags.contains('9')
97    }
98
99    pub fn is_complete(&self) -> bool {
100        !self.has_missing_data()
101    }
102}
103
104pub struct ConsolidatedRecordBuilder {
105    station_id: Option<u32>,
106    station_name: Option<String>,
107    date: Option<NaiveDate>,
108    latitude: Option<f64>,
109    longitude: Option<f64>,
110    min_temp: Option<f32>,
111    max_temp: Option<f32>,
112    avg_temp: Option<f32>,
113    quality_flags: Option<String>,
114}
115
116impl Default for ConsolidatedRecordBuilder {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl ConsolidatedRecordBuilder {
123    pub fn new() -> Self {
124        Self {
125            station_id: None,
126            station_name: None,
127            date: None,
128            latitude: None,
129            longitude: None,
130            min_temp: None,
131            max_temp: None,
132            avg_temp: None,
133            quality_flags: None,
134        }
135    }
136
137    pub fn station_id(mut self, id: u32) -> Self {
138        self.station_id = Some(id);
139        self
140    }
141
142    pub fn station_name(mut self, name: String) -> Self {
143        self.station_name = Some(name);
144        self
145    }
146
147    pub fn date(mut self, date: NaiveDate) -> Self {
148        self.date = Some(date);
149        self
150    }
151
152    pub fn coordinates(mut self, latitude: f64, longitude: f64) -> Self {
153        self.latitude = Some(latitude);
154        self.longitude = Some(longitude);
155        self
156    }
157
158    pub fn temperatures(mut self, min: f32, avg: f32, max: f32) -> Self {
159        self.min_temp = Some(min);
160        self.avg_temp = Some(avg);
161        self.max_temp = Some(max);
162        self
163    }
164
165    pub fn quality_flags(mut self, flags: String) -> Self {
166        self.quality_flags = Some(flags);
167        self
168    }
169
170    pub fn build(self) -> Result<ConsolidatedRecord> {
171        let record = ConsolidatedRecord::new(
172            self.station_id
173                .ok_or_else(|| ProcessingError::MissingData("station_id".to_string()))?,
174            self.station_name
175                .ok_or_else(|| ProcessingError::MissingData("station_name".to_string()))?,
176            self.date
177                .ok_or_else(|| ProcessingError::MissingData("date".to_string()))?,
178            self.latitude
179                .ok_or_else(|| ProcessingError::MissingData("latitude".to_string()))?,
180            self.longitude
181                .ok_or_else(|| ProcessingError::MissingData("longitude".to_string()))?,
182            self.min_temp
183                .ok_or_else(|| ProcessingError::MissingData("min_temp".to_string()))?,
184            self.max_temp
185                .ok_or_else(|| ProcessingError::MissingData("max_temp".to_string()))?,
186            self.avg_temp
187                .ok_or_else(|| ProcessingError::MissingData("avg_temp".to_string()))?,
188            self.quality_flags
189                .ok_or_else(|| ProcessingError::MissingData("quality_flags".to_string()))?,
190        );
191
192        record.validate_relationships()?;
193        Ok(record)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_consolidated_record_validation() {
203        let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
204
205        let record = ConsolidatedRecord::new(
206            12345,
207            "London Station".to_string(),
208            date,
209            51.5074,
210            -0.1278,
211            10.0,
212            20.0,
213            15.0,
214            "000".to_string(),
215        );
216
217        assert!(record.validate_relationships().is_ok());
218        assert!(record.has_valid_data());
219        assert!(!record.has_suspect_data());
220        assert!(!record.has_missing_data());
221        assert!(record.is_complete());
222        assert_eq!(record.temperature_range(), 10.0);
223    }
224
225    #[test]
226    fn test_invalid_temperature_relationship() {
227        let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
228
229        let record = ConsolidatedRecord::new(
230            12345,
231            "London Station".to_string(),
232            date,
233            51.5074,
234            -0.1278,
235            20.0, // min > avg
236            10.0, // max < avg
237            15.0,
238            "000".to_string(),
239        );
240
241        assert!(record.validate_relationships().is_err());
242    }
243
244    #[test]
245    fn test_builder_pattern() {
246        let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
247
248        let record = ConsolidatedRecordBuilder::new()
249            .station_id(12345)
250            .station_name("Test Station".to_string())
251            .date(date)
252            .coordinates(51.5074, -0.1278)
253            .temperatures(10.0, 15.0, 20.0)
254            .quality_flags("000".to_string())
255            .build()
256            .unwrap();
257
258        assert_eq!(record.station_id, 12345);
259        assert_eq!(record.station_name, "Test Station");
260        assert!(record.validate_relationships().is_ok());
261    }
262}