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 temp_set.avg = Some(TemperatureRecord::new(1, 1, date, 25.0, 0).unwrap());
206 assert!(temp_set.validate_relationships().is_err());
207 }
208}