Skip to main content

nexrad_data/aws/realtime/
chunk_identifier.rs

1use crate::aws::realtime::{ChunkType, VolumeIndex};
2use chrono::{DateTime, Utc};
3
4/// Identifies a volume chunk within the real-time NEXRAD data bucket. These chunks are uploaded
5/// every few seconds and contain a portion of the radar data for a specific volume.
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
7pub struct ChunkIdentifier {
8    site: String,
9    volume: VolumeIndex,
10    name: String,
11    date_time: Option<DateTime<Utc>>,
12}
13
14impl ChunkIdentifier {
15    /// Creates a new chunk identifier.
16    pub fn new(
17        site: String,
18        volume: VolumeIndex,
19        name: String,
20        date_time: Option<DateTime<Utc>>,
21    ) -> Self {
22        Self {
23            site,
24            volume,
25            name,
26            date_time,
27        }
28    }
29
30    /// Creates a new chunk identifier with the given sequence number. The chunk type will be
31    /// inferred from the sequence and date/time will be omitted since it is unknown.
32    pub fn with_sequence(&self, sequence: usize) -> Self {
33        let name = format!(
34            "{}-{:03}-{}",
35            self.name_prefix(),
36            sequence,
37            match sequence {
38                1 => "S",
39                55 => "E",
40                _ => "I",
41            }
42        );
43
44        Self {
45            site: self.site.clone(),
46            volume: self.volume,
47            name,
48            date_time: None,
49        }
50    }
51
52    /// The chunk's radar site identifier.
53    pub fn site(&self) -> &str {
54        &self.site
55    }
56
57    /// The chunk's rotating volume index.
58    pub fn volume(&self) -> &VolumeIndex {
59        &self.volume
60    }
61
62    /// The chunk's name.
63    pub fn name(&self) -> &str {
64        &self.name
65    }
66
67    /// The chunk's name prefix.
68    pub fn name_prefix(&self) -> &str {
69        &self.name[..15]
70    }
71
72    /// The sequence number of this chunk within the volume.
73    pub fn sequence(&self) -> Option<usize> {
74        self.name.split('-').nth(2).and_then(|s| s.parse().ok())
75    }
76
77    /// The position of this chunk within the volume.
78    pub fn chunk_type(&self) -> Option<ChunkType> {
79        match self.name.chars().last() {
80            Some('S') => Some(ChunkType::Start),
81            Some('I') => Some(ChunkType::Intermediate),
82            Some('E') => Some(ChunkType::End),
83            _ => None,
84        }
85    }
86
87    /// The date and time this chunk was uploaded.
88    pub fn date_time(&self) -> Option<DateTime<Utc>> {
89        self.date_time
90    }
91
92    /// Identifies the next chunk's expected location.
93    pub fn next_chunk(&self) -> Option<NextChunk> {
94        let sequence = self.sequence()?;
95        let mut chunk_type;
96
97        if sequence < 55 {
98            let next_sequence = sequence + 1;
99
100            chunk_type = ChunkType::Intermediate;
101            if next_sequence == 55 {
102                chunk_type = ChunkType::End;
103            }
104
105            let name = format!(
106                "{}-{:03}-{}",
107                self.name_prefix(),
108                next_sequence,
109                match chunk_type {
110                    ChunkType::Start => "S",
111                    ChunkType::Intermediate => "I",
112                    ChunkType::End => "E",
113                }
114            );
115
116            let next_chunk = ChunkIdentifier::new(self.site().to_string(), self.volume, name, None);
117            return Some(NextChunk::Sequence(next_chunk));
118        }
119
120        let mut volume = self.volume.as_number() + 1;
121        if volume > 999 {
122            volume = 1;
123        }
124
125        Some(NextChunk::Volume(VolumeIndex::new(volume)))
126    }
127}
128
129/// Identifies where to find the next expected chunk.
130#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131pub enum NextChunk {
132    /// The next chunk is expected to be located in the same volume at this sequence. The
133    /// [ChunkIdentifier::with_sequence] method can be used to create the next chunk's identifier
134    /// and it can be downloaded using the [crate::aws::realtime::download_chunk()] function. You
135    /// may need to poll by checking if that function returns
136    /// [crate::result::aws::AWSError::S3ObjectNotFoundError].
137    Sequence(ChunkIdentifier),
138
139    /// The chunk is expected to be located in the next volume. The next volume's chunks can be
140    /// listed using the [crate::aws::realtime::list_chunks_in_volume()] function.
141    Volume(VolumeIndex),
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    use chrono::TimeZone;
149
150    #[test]
151    fn test_chunk_identifier() {
152        let site = "KTLX";
153        let volume = 50;
154        let name = "20240813-123330-014-I";
155        let date_time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
156
157        let chunk = ChunkIdentifier::new(
158            site.to_string(),
159            VolumeIndex::new(volume),
160            name.to_string(),
161            Some(date_time),
162        );
163
164        assert_eq!(chunk.site(), site);
165        assert_eq!(chunk.volume().as_number(), 50);
166        assert_eq!(chunk.name(), name);
167        assert_eq!(chunk.name_prefix(), "20240813-123330");
168        assert_eq!(chunk.chunk_type(), Some(ChunkType::Intermediate));
169        assert_eq!(chunk.sequence(), Some(14));
170        assert_eq!(chunk.date_time(), Some(date_time));
171    }
172
173    #[test]
174    fn test_next_chunk_start() {
175        let site = "KTLX";
176        let volume = 50;
177        let name = "20240813-123330-001-S";
178        let date_time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
179
180        let chunk = ChunkIdentifier::new(
181            site.to_string(),
182            VolumeIndex::new(volume),
183            name.to_string(),
184            Some(date_time),
185        );
186
187        let next_chunk = chunk.next_chunk().expect("Expected next chunk");
188        match next_chunk {
189            NextChunk::Sequence(next_chunk) => {
190                assert_eq!(next_chunk.site(), site);
191                assert_eq!(next_chunk.volume().as_number(), 50);
192                assert_eq!(next_chunk.name(), "20240813-123330-002-I");
193                assert_eq!(next_chunk.name_prefix(), "20240813-123330");
194                assert_eq!(next_chunk.sequence(), Some(2));
195                assert_eq!(next_chunk.chunk_type(), Some(ChunkType::Intermediate));
196                assert_eq!(next_chunk.date_time(), None);
197            }
198            _ => panic!("Expected sequence"),
199        }
200    }
201
202    #[test]
203    fn test_next_chunk_intermediate() {
204        let site = "KTLX";
205        let volume = 999;
206        let name = "20240813-123330-014-I";
207        let date_time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
208
209        let chunk = ChunkIdentifier::new(
210            site.to_string(),
211            VolumeIndex::new(volume),
212            name.to_string(),
213            Some(date_time),
214        );
215
216        let next_chunk = chunk.next_chunk().expect("Expected next chunk");
217        match next_chunk {
218            NextChunk::Sequence(next_chunk) => {
219                assert_eq!(next_chunk.site(), site);
220                assert_eq!(next_chunk.volume().as_number(), 999);
221                assert_eq!(next_chunk.name(), "20240813-123330-015-I");
222                assert_eq!(next_chunk.name_prefix(), "20240813-123330");
223                assert_eq!(next_chunk.sequence(), Some(15));
224                assert_eq!(next_chunk.chunk_type(), Some(ChunkType::Intermediate));
225                assert_eq!(next_chunk.date_time(), None);
226            }
227            _ => panic!("Expected sequence"),
228        }
229    }
230
231    #[test]
232    fn test_next_chunk_end() {
233        let site = "KTLX";
234        let volume = 50;
235        let name = "20240813-123330-055-E";
236        let date_time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
237
238        let chunk = ChunkIdentifier::new(
239            site.to_string(),
240            VolumeIndex::new(volume),
241            name.to_string(),
242            Some(date_time),
243        );
244
245        let next_chunk = chunk.next_chunk().expect("Expected next chunk");
246        match next_chunk {
247            NextChunk::Volume(next_volume) => {
248                assert_eq!(next_volume.as_number(), 51);
249            }
250            _ => panic!("Expected volume"),
251        }
252    }
253
254    #[test]
255    fn test_next_chunk_last_volume() {
256        let site = "KTLX";
257        let volume = 999;
258        let name = "20240813-123330-055-E";
259        let date_time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
260
261        let chunk = ChunkIdentifier::new(
262            site.to_string(),
263            VolumeIndex::new(volume),
264            name.to_string(),
265            Some(date_time),
266        );
267
268        let next_chunk = chunk.next_chunk().expect("Expected next chunk");
269        match next_chunk {
270            NextChunk::Volume(next_volume) => {
271                assert_eq!(next_volume.as_number(), 1);
272            }
273            _ => panic!("Expected volume"),
274        }
275    }
276
277    #[test]
278    fn test_chunk_from_sequence() {
279        let site = "KTLX";
280        let volume = 50;
281        let name = "20240813-123330-014-I";
282        let date_time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
283
284        let chunk = ChunkIdentifier::new(
285            site.to_string(),
286            VolumeIndex::new(volume),
287            name.to_string(),
288            Some(date_time),
289        );
290
291        let next_chunk = chunk.with_sequence(15);
292        assert_eq!(next_chunk.site(), site);
293        assert_eq!(next_chunk.volume().as_number(), 50);
294        assert_eq!(next_chunk.name(), "20240813-123330-015-I");
295        assert_eq!(next_chunk.name_prefix(), "20240813-123330");
296        assert_eq!(next_chunk.sequence(), Some(15));
297        assert_eq!(next_chunk.chunk_type(), Some(ChunkType::Intermediate));
298        assert_eq!(next_chunk.date_time(), None);
299
300        assert_eq!(chunk.with_sequence(1).chunk_type(), Some(ChunkType::Start));
301        assert_eq!(chunk.with_sequence(55).chunk_type(), Some(ChunkType::End));
302    }
303}