1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3use validator::Validate;
4
5use crate::error::{ProcessingError, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
8pub enum PhysicalValidity {
9 Valid, Suspect, Invalid, }
13
14impl PhysicalValidity {
15 pub fn parse(s: &str) -> Option<Self> {
16 match s {
17 "Valid" => Some(PhysicalValidity::Valid),
18 "Suspect" => Some(PhysicalValidity::Suspect),
19 "Invalid" => Some(PhysicalValidity::Invalid),
20 _ => None,
21 }
22 }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
26pub enum DataQuality {
27 Valid, SuspectOriginal, SuspectRange, SuspectBoth, Invalid, Missing, }
34
35#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
36pub struct WeatherRecord {
37 pub station_id: u32,
39 pub station_name: String,
40 pub date: NaiveDate,
41
42 #[validate(range(min = -90.0, max = 90.0))]
43 pub latitude: f64,
44
45 #[validate(range(min = -180.0, max = 180.0))]
46 pub longitude: f64,
47
48 #[validate(range(min = -50.0, max = 50.0))]
50 pub temp_min: Option<f32>,
51
52 #[validate(range(min = -50.0, max = 50.0))]
53 pub temp_max: Option<f32>,
54
55 #[validate(range(min = -50.0, max = 50.0))]
56 pub temp_avg: Option<f32>,
57
58 #[validate(range(min = 0.0, max = 1000.0))]
60 pub precipitation: Option<f32>,
61
62 #[validate(range(min = 0.0, max = 100.0))]
64 pub wind_speed: Option<f32>,
65
66 pub temp_quality: Option<String>, pub precip_quality: Option<String>,
69 pub wind_quality: Option<String>,
70
71 pub temp_validation: Option<PhysicalValidity>,
73 pub precip_validation: Option<PhysicalValidity>,
74 pub wind_validation: Option<PhysicalValidity>,
75}
76
77impl WeatherRecord {
78 #[allow(clippy::too_many_arguments)]
79 pub fn new(
80 station_id: u32,
81 station_name: String,
82 date: NaiveDate,
83 latitude: f64,
84 longitude: f64,
85 temp_min: Option<f32>,
86 temp_max: Option<f32>,
87 temp_avg: Option<f32>,
88 precipitation: Option<f32>,
89 wind_speed: Option<f32>,
90 temp_quality: Option<String>,
91 precip_quality: Option<String>,
92 wind_quality: Option<String>,
93 ) -> Self {
94 let mut record = Self {
95 station_id,
96 station_name,
97 date,
98 latitude,
99 longitude,
100 temp_min,
101 temp_max,
102 temp_avg,
103 precipitation,
104 wind_speed,
105 temp_quality,
106 precip_quality,
107 wind_quality,
108 temp_validation: None,
109 precip_validation: None,
110 wind_validation: None,
111 };
112
113 record.perform_physical_validation();
115 record
116 }
117
118 #[allow(clippy::too_many_arguments)]
120 pub fn new_raw(
121 station_id: u32,
122 station_name: String,
123 date: NaiveDate,
124 latitude: f64,
125 longitude: f64,
126 temp_min: Option<f32>,
127 temp_max: Option<f32>,
128 temp_avg: Option<f32>,
129 precipitation: Option<f32>,
130 wind_speed: Option<f32>,
131 temp_quality: Option<String>,
132 precip_quality: Option<String>,
133 wind_quality: Option<String>,
134 temp_validation: Option<PhysicalValidity>,
135 precip_validation: Option<PhysicalValidity>,
136 wind_validation: Option<PhysicalValidity>,
137 ) -> Self {
138 Self {
139 station_id,
140 station_name,
141 date,
142 latitude,
143 longitude,
144 temp_min,
145 temp_max,
146 temp_avg,
147 precipitation,
148 wind_speed,
149 temp_quality,
150 precip_quality,
151 wind_quality,
152 temp_validation,
153 precip_validation,
154 wind_validation,
155 }
156 }
157
158 pub fn builder() -> WeatherRecordBuilder {
159 WeatherRecordBuilder::new()
160 }
161
162 pub fn validate_relationships(&self) -> Result<()> {
163 if let (Some(min), Some(avg), Some(max)) = (self.temp_min, self.temp_avg, self.temp_max) {
165 let tolerance = 1.0; if min > avg + tolerance {
168 return Err(ProcessingError::TemperatureValidation {
169 message: format!(
170 "Min temperature {} > Avg temperature {} (tolerance={})",
171 min, avg, tolerance
172 ),
173 });
174 }
175
176 if avg > max + tolerance {
177 return Err(ProcessingError::TemperatureValidation {
178 message: format!(
179 "Avg temperature {} > Max temperature {} (tolerance={})",
180 avg, max, tolerance
181 ),
182 });
183 }
184 }
185
186 self.validate()?;
187 Ok(())
188 }
189
190 pub fn has_temperature_data(&self) -> bool {
191 self.temp_min.is_some() || self.temp_max.is_some() || self.temp_avg.is_some()
192 }
193
194 pub fn has_complete_temperature(&self) -> bool {
195 self.temp_min.is_some() && self.temp_max.is_some() && self.temp_avg.is_some()
196 }
197
198 pub fn has_precipitation(&self) -> bool {
199 self.precipitation.is_some()
200 }
201
202 pub fn has_wind_speed(&self) -> bool {
203 self.wind_speed.is_some()
204 }
205
206 pub fn available_metrics(&self) -> Vec<&str> {
207 let mut metrics = Vec::new();
208 if self.has_temperature_data() {
209 metrics.push("temperature");
210 }
211 if self.has_precipitation() {
212 metrics.push("precipitation");
213 }
214 if self.has_wind_speed() {
215 metrics.push("wind_speed");
216 }
217 metrics
218 }
219
220 pub fn metric_coverage_score(&self) -> f32 {
221 let total_metrics = 3.0; let available = self.available_metrics().len() as f32;
223 available / total_metrics
224 }
225
226 pub fn temperature_range(&self) -> Option<f32> {
227 match (self.temp_min, self.temp_max) {
228 (Some(min), Some(max)) => Some(max - min),
229 _ => None,
230 }
231 }
232
233 pub fn has_valid_temperature_data(&self) -> bool {
234 self.temp_quality.as_ref().is_some_and(|q| q == "000")
235 }
236
237 pub fn has_valid_precipitation_data(&self) -> bool {
238 self.precip_quality.as_ref().is_some_and(|q| q == "0")
239 }
240
241 pub fn has_valid_wind_data(&self) -> bool {
242 self.wind_quality.as_ref().is_some_and(|q| q == "0")
243 }
244
245 pub fn has_suspect_data(&self) -> bool {
246 self.temp_quality.as_ref().is_some_and(|q| q.contains('1'))
247 || self
248 .precip_quality
249 .as_ref()
250 .is_some_and(|q| q.contains('1'))
251 || self.wind_quality.as_ref().is_some_and(|q| q.contains('1'))
252 }
253
254 pub fn has_missing_data(&self) -> bool {
255 self.temp_quality.as_ref().is_some_and(|q| q.contains('9'))
256 || self
257 .precip_quality
258 .as_ref()
259 .is_some_and(|q| q.contains('9'))
260 || self.wind_quality.as_ref().is_some_and(|q| q.contains('9'))
261 }
262
263 pub fn perform_physical_validation(&mut self) {
265 self.temp_validation = self.validate_temperature_physics();
266 self.precip_validation = self.validate_precipitation_physics();
267 self.wind_validation = self.validate_wind_physics();
268 }
269
270 fn validate_temperature_physics(&self) -> Option<PhysicalValidity> {
272 let temps = [self.temp_min, self.temp_max, self.temp_avg];
273 let existing_temps: Vec<f32> = temps.into_iter().flatten().collect();
274
275 if existing_temps.is_empty() {
276 return None;
277 }
278
279 for &temp in &existing_temps {
280 if !(-90.0..=60.0).contains(&temp) {
282 return Some(PhysicalValidity::Invalid);
283 }
284
285 if !(-35.0..=45.0).contains(&temp) {
287 return Some(PhysicalValidity::Suspect);
288 }
289 }
290
291 Some(PhysicalValidity::Valid)
292 }
293
294 fn validate_precipitation_physics(&self) -> Option<PhysicalValidity> {
296 if let Some(precip) = self.precipitation {
297 if !(0.0..=2000.0).contains(&precip) {
299 return Some(PhysicalValidity::Invalid);
300 }
301
302 if precip > 500.0 {
304 return Some(PhysicalValidity::Suspect);
305 }
306
307 Some(PhysicalValidity::Valid)
308 } else {
309 None
310 }
311 }
312
313 fn validate_wind_physics(&self) -> Option<PhysicalValidity> {
315 if let Some(wind) = self.wind_speed {
316 if !(0.0..=120.0).contains(&wind) {
318 return Some(PhysicalValidity::Invalid);
319 }
320
321 if wind > 50.0 {
323 return Some(PhysicalValidity::Suspect);
324 }
325
326 Some(PhysicalValidity::Valid)
327 } else {
328 None
329 }
330 }
331
332 pub fn assess_temperature_quality(&self) -> DataQuality {
334 match (self.temp_quality.as_deref(), self.temp_validation) {
335 (Some(q), _) if q.contains('9') => DataQuality::Missing,
336 (_, Some(PhysicalValidity::Invalid)) => DataQuality::Invalid,
337 (Some(q), Some(PhysicalValidity::Suspect)) if q.contains('1') => {
338 DataQuality::SuspectBoth
339 }
340 (Some(q), _) if q.contains('1') => DataQuality::SuspectOriginal,
341 (_, Some(PhysicalValidity::Suspect)) => DataQuality::SuspectRange,
342 _ => DataQuality::Valid,
343 }
344 }
345
346 pub fn assess_precipitation_quality(&self) -> DataQuality {
348 match (self.precip_quality.as_deref(), self.precip_validation) {
349 (Some("9"), _) => DataQuality::Missing,
350 (_, Some(PhysicalValidity::Invalid)) => DataQuality::Invalid,
351 (Some("1"), Some(PhysicalValidity::Suspect)) => DataQuality::SuspectBoth,
352 (Some("1"), _) => DataQuality::SuspectOriginal,
353 (_, Some(PhysicalValidity::Suspect)) => DataQuality::SuspectRange,
354 _ => DataQuality::Valid,
355 }
356 }
357
358 pub fn assess_wind_quality(&self) -> DataQuality {
360 match (self.wind_quality.as_deref(), self.wind_validation) {
361 (Some("9"), _) => DataQuality::Missing,
362 (_, Some(PhysicalValidity::Invalid)) => DataQuality::Invalid,
363 (Some("1"), Some(PhysicalValidity::Suspect)) => DataQuality::SuspectBoth,
364 (Some("1"), _) => DataQuality::SuspectOriginal,
365 (_, Some(PhysicalValidity::Suspect)) => DataQuality::SuspectRange,
366 _ => DataQuality::Valid,
367 }
368 }
369
370 pub fn has_invalid_data(&self) -> bool {
372 matches!(self.assess_temperature_quality(), DataQuality::Invalid)
373 || matches!(self.assess_precipitation_quality(), DataQuality::Invalid)
374 || matches!(self.assess_wind_quality(), DataQuality::Invalid)
375 }
376
377 pub fn has_high_quality_data(&self) -> bool {
379 let temp_ok = self.temp_validation.is_none()
380 || matches!(self.assess_temperature_quality(), DataQuality::Valid);
381 let precip_ok = self.precip_validation.is_none()
382 || matches!(self.assess_precipitation_quality(), DataQuality::Valid);
383 let wind_ok = self.wind_validation.is_none()
384 || matches!(self.assess_wind_quality(), DataQuality::Valid);
385
386 temp_ok && precip_ok && wind_ok
387 }
388}
389
390pub struct WeatherRecordBuilder {
391 station_id: Option<u32>,
392 station_name: Option<String>,
393 date: Option<NaiveDate>,
394 latitude: Option<f64>,
395 longitude: Option<f64>,
396 temp_min: Option<f32>,
397 temp_max: Option<f32>,
398 temp_avg: Option<f32>,
399 precipitation: Option<f32>,
400 wind_speed: Option<f32>,
401 temp_quality: Option<String>,
402 precip_quality: Option<String>,
403 wind_quality: Option<String>,
404 temp_validation: Option<PhysicalValidity>,
405 precip_validation: Option<PhysicalValidity>,
406 wind_validation: Option<PhysicalValidity>,
407}
408
409impl Default for WeatherRecordBuilder {
410 fn default() -> Self {
411 Self::new()
412 }
413}
414
415impl WeatherRecordBuilder {
416 pub fn new() -> Self {
417 Self {
418 station_id: None,
419 station_name: None,
420 date: None,
421 latitude: None,
422 longitude: None,
423 temp_min: None,
424 temp_max: None,
425 temp_avg: None,
426 precipitation: None,
427 wind_speed: None,
428 temp_quality: None,
429 precip_quality: None,
430 wind_quality: None,
431 temp_validation: None,
432 precip_validation: None,
433 wind_validation: None,
434 }
435 }
436
437 pub fn station_id(mut self, id: u32) -> Self {
438 self.station_id = Some(id);
439 self
440 }
441
442 pub fn station_name(mut self, name: String) -> Self {
443 self.station_name = Some(name);
444 self
445 }
446
447 pub fn date(mut self, date: NaiveDate) -> Self {
448 self.date = Some(date);
449 self
450 }
451
452 pub fn coordinates(mut self, latitude: f64, longitude: f64) -> Self {
453 self.latitude = Some(latitude);
454 self.longitude = Some(longitude);
455 self
456 }
457
458 pub fn temp_min(mut self, temp: f32) -> Self {
459 self.temp_min = Some(temp);
460 self
461 }
462
463 pub fn temp_max(mut self, temp: f32) -> Self {
464 self.temp_max = Some(temp);
465 self
466 }
467
468 pub fn temp_avg(mut self, temp: f32) -> Self {
469 self.temp_avg = Some(temp);
470 self
471 }
472
473 pub fn temperatures(mut self, min: f32, avg: f32, max: f32) -> Self {
474 self.temp_min = Some(min);
475 self.temp_avg = Some(avg);
476 self.temp_max = Some(max);
477 self
478 }
479
480 pub fn precipitation(mut self, precip: f32) -> Self {
481 self.precipitation = Some(precip);
482 self
483 }
484
485 pub fn wind_speed(mut self, speed: f32) -> Self {
486 self.wind_speed = Some(speed);
487 self
488 }
489
490 pub fn temp_quality(mut self, quality: String) -> Self {
491 self.temp_quality = Some(quality);
492 self
493 }
494
495 pub fn precip_quality(mut self, quality: String) -> Self {
496 self.precip_quality = Some(quality);
497 self
498 }
499
500 pub fn wind_quality(mut self, quality: String) -> Self {
501 self.wind_quality = Some(quality);
502 self
503 }
504
505 pub fn build(self) -> Result<WeatherRecord> {
506 let mut record = WeatherRecord {
507 station_id: self
508 .station_id
509 .ok_or_else(|| ProcessingError::MissingData("station_id".to_string()))?,
510 station_name: self
511 .station_name
512 .ok_or_else(|| ProcessingError::MissingData("station_name".to_string()))?,
513 date: self
514 .date
515 .ok_or_else(|| ProcessingError::MissingData("date".to_string()))?,
516 latitude: self
517 .latitude
518 .ok_or_else(|| ProcessingError::MissingData("latitude".to_string()))?,
519 longitude: self
520 .longitude
521 .ok_or_else(|| ProcessingError::MissingData("longitude".to_string()))?,
522 temp_min: self.temp_min,
523 temp_max: self.temp_max,
524 temp_avg: self.temp_avg,
525 precipitation: self.precipitation,
526 wind_speed: self.wind_speed,
527 temp_quality: self.temp_quality,
528 precip_quality: self.precip_quality,
529 wind_quality: self.wind_quality,
530 temp_validation: self.temp_validation,
531 precip_validation: self.precip_validation,
532 wind_validation: self.wind_validation,
533 };
534
535 if record.temp_validation.is_none()
537 || record.precip_validation.is_none()
538 || record.wind_validation.is_none()
539 {
540 record.perform_physical_validation();
541 }
542
543 record.validate_relationships()?;
544 Ok(record)
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn test_weather_record_creation() {
554 let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
555
556 let record = WeatherRecord::new(
557 12345,
558 "London Station".to_string(),
559 date,
560 51.5074,
561 -0.1278,
562 Some(10.0),
563 Some(20.0),
564 Some(15.0),
565 Some(5.0),
566 Some(3.2),
567 Some("000".to_string()),
568 Some("0".to_string()),
569 Some("0".to_string()),
570 );
571
572 assert_eq!(record.station_id, 12345);
573 assert_eq!(record.station_name, "London Station");
574 assert!(record.has_complete_temperature());
575 assert!(record.has_precipitation());
576 assert!(record.has_wind_speed());
577 assert_eq!(record.available_metrics().len(), 3);
578 assert_eq!(record.metric_coverage_score(), 1.0);
579 }
580
581 #[test]
582 fn test_temperature_validation() {
583 let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
584
585 let record = WeatherRecord::new(
586 12345,
587 "Test Station".to_string(),
588 date,
589 51.5074,
590 -0.1278,
591 Some(10.0),
592 Some(20.0),
593 Some(15.0),
594 None,
595 None,
596 Some("000".to_string()),
597 None,
598 None,
599 );
600
601 assert!(record.validate_relationships().is_ok());
602 assert_eq!(record.temperature_range(), Some(10.0));
603 }
604
605 #[test]
606 fn test_invalid_temperature_relationship() {
607 let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
608
609 let record = WeatherRecord::new(
610 12345,
611 "Test Station".to_string(),
612 date,
613 51.5074,
614 -0.1278,
615 Some(20.0), Some(10.0), Some(15.0),
618 None,
619 None,
620 Some("000".to_string()),
621 None,
622 None,
623 );
624
625 assert!(record.validate_relationships().is_err());
626 }
627
628 #[test]
629 fn test_builder_pattern() {
630 let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
631
632 let record = WeatherRecord::builder()
633 .station_id(12345)
634 .station_name("Test Station".to_string())
635 .date(date)
636 .coordinates(51.5074, -0.1278)
637 .temperatures(10.0, 15.0, 20.0)
638 .precipitation(5.5)
639 .wind_speed(3.2)
640 .temp_quality("000".to_string())
641 .build()
642 .unwrap();
643
644 assert_eq!(record.station_id, 12345);
645 assert_eq!(record.station_name, "Test Station");
646 assert!(record.validate_relationships().is_ok());
647 assert!(record.has_complete_temperature());
648 assert!(record.has_precipitation());
649 assert!(record.has_wind_speed());
650 }
651
652 #[test]
653 fn test_partial_data() {
654 let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
655
656 let record = WeatherRecord::builder()
657 .station_id(12345)
658 .station_name("Test Station".to_string())
659 .date(date)
660 .coordinates(51.5074, -0.1278)
661 .temp_min(10.0)
662 .precipitation(5.5)
663 .build()
664 .unwrap();
665
666 assert!(record.has_temperature_data());
667 assert!(!record.has_complete_temperature());
668 assert!(record.has_precipitation());
669 assert!(!record.has_wind_speed());
670 assert_eq!(record.available_metrics().len(), 2);
671 assert!((record.metric_coverage_score() - 0.666).abs() < 0.01);
672 }
673
674 #[test]
675 fn test_quality_flags() {
676 let date = NaiveDate::from_ymd_opt(2023, 7, 15).unwrap();
677
678 let record = WeatherRecord::builder()
679 .station_id(12345)
680 .station_name("Test Station".to_string())
681 .date(date)
682 .coordinates(51.5074, -0.1278)
683 .temp_min(10.0)
684 .temp_quality("001".to_string()) .build()
686 .unwrap();
687
688 assert!(!record.has_valid_temperature_data());
689 assert!(record.has_suspect_data());
690 assert!(!record.has_missing_data());
691 }
692}