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; 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, 10.0, 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}