Skip to main content

grib_core/
product.rs

1//! GRIB2 metadata carried by Sections 1 and 4.
2
3use crate::error::{Error, Result};
4use crate::metadata::ReferenceTime;
5use crate::parameter;
6use crate::util::{grib_i32, grib_i8};
7
8/// Section 1: Identification Section.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Identification {
11    pub center_id: u16,
12    pub subcenter_id: u16,
13    pub master_table_version: u8,
14    pub local_table_version: u8,
15    pub significance_of_reference_time: u8,
16    pub reference_year: u16,
17    pub reference_month: u8,
18    pub reference_day: u8,
19    pub reference_hour: u8,
20    pub reference_minute: u8,
21    pub reference_second: u8,
22    pub production_status: u8,
23    pub processed_data_type: u8,
24}
25
26impl Identification {
27    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
28        if section_bytes.len() < 21 {
29            return Err(Error::InvalidSection {
30                section: 1,
31                reason: format!("expected at least 21 bytes, got {}", section_bytes.len()),
32            });
33        }
34        if section_bytes[4] != 1 {
35            return Err(Error::InvalidSection {
36                section: section_bytes[4],
37                reason: "not an identification section".into(),
38            });
39        }
40
41        let reference_time = ReferenceTime {
42            year: u16::from_be_bytes(section_bytes[12..14].try_into().unwrap()),
43            month: section_bytes[14],
44            day: section_bytes[15],
45            hour: section_bytes[16],
46            minute: section_bytes[17],
47            second: section_bytes[18],
48        };
49        reference_time.validate_in_section(1)?;
50
51        Ok(Self {
52            center_id: u16::from_be_bytes(section_bytes[5..7].try_into().unwrap()),
53            subcenter_id: u16::from_be_bytes(section_bytes[7..9].try_into().unwrap()),
54            master_table_version: section_bytes[9],
55            local_table_version: section_bytes[10],
56            significance_of_reference_time: section_bytes[11],
57            reference_year: reference_time.year,
58            reference_month: reference_time.month,
59            reference_day: reference_time.day,
60            reference_hour: reference_time.hour,
61            reference_minute: reference_time.minute,
62            reference_second: reference_time.second,
63            production_status: section_bytes[19],
64            processed_data_type: section_bytes[20],
65        })
66    }
67}
68
69/// A fixed surface from Product Definition templates.
70#[derive(Debug, Clone, PartialEq)]
71pub struct FixedSurface {
72    pub surface_type: u8,
73    pub scale_factor: i16,
74    pub scaled_value: i32,
75}
76
77impl FixedSurface {
78    pub fn scaled_value_f64(&self) -> f64 {
79        let factor = 10.0_f64.powi(-(self.scale_factor as i32));
80        self.scaled_value as f64 * factor
81    }
82}
83
84/// Section 4: Product Definition Section.
85#[derive(Debug, Clone, PartialEq)]
86pub struct ProductDefinition {
87    pub parameter_category: u8,
88    pub parameter_number: u8,
89    pub template: ProductDefinitionTemplate,
90}
91
92/// Typed GRIB2 Product Definition templates.
93#[derive(Debug, Clone, PartialEq)]
94pub enum ProductDefinitionTemplate {
95    AnalysisOrForecast(AnalysisOrForecastTemplate),
96    IndividualEnsembleForecast(IndividualEnsembleForecastTemplate),
97    StatisticalProcess(StatisticalProcessTemplate),
98    EnsembleStatisticalProcess(EnsembleStatisticalProcessTemplate),
99}
100
101/// Product Definition Template 4.0: analysis or forecast at a horizontal level.
102#[derive(Debug, Clone, PartialEq)]
103pub struct AnalysisOrForecastTemplate {
104    pub generating_process: u8,
105    pub forecast_time_unit: u8,
106    pub forecast_time: u32,
107    pub first_surface: Option<FixedSurface>,
108    pub second_surface: Option<FixedSurface>,
109}
110
111/// Product Definition Template 4.1: individual ensemble forecast at a point in time.
112#[derive(Debug, Clone, PartialEq)]
113pub struct IndividualEnsembleForecastTemplate {
114    pub base: AnalysisOrForecastTemplate,
115    pub type_of_ensemble_forecast: u8,
116    pub perturbation_number: u8,
117    pub number_of_forecasts_in_ensemble: u8,
118}
119
120/// Product Definition Template 4.8: statistically processed field over a time interval.
121#[derive(Debug, Clone, PartialEq)]
122pub struct StatisticalProcessTemplate {
123    pub base: AnalysisOrForecastTemplate,
124    pub end_of_overall_time_interval: ReferenceTime,
125    pub number_of_missing_in_statistical_process: u32,
126    pub time_ranges: Vec<StatisticalTimeRange>,
127}
128
129/// Product Definition Template 4.11: individual ensemble forecast over a time interval.
130#[derive(Debug, Clone, PartialEq)]
131pub struct EnsembleStatisticalProcessTemplate {
132    pub ensemble: IndividualEnsembleForecastTemplate,
133    pub end_of_overall_time_interval: ReferenceTime,
134    pub number_of_missing_in_statistical_process: u32,
135    pub time_ranges: Vec<StatisticalTimeRange>,
136}
137
138/// Statistical processing descriptor from GRIB2 Product Definition templates
139/// with one or more time range specifications.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct StatisticalTimeRange {
142    pub type_of_statistical_processing: u8,
143    pub type_of_time_increment: u8,
144    pub time_range_unit: u8,
145    pub time_range_length: u32,
146    pub time_increment_unit: u8,
147    pub time_increment: u32,
148}
149
150impl ProductDefinition {
151    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
152        if section_bytes.len() < 11 {
153            return Err(Error::InvalidSection {
154                section: 4,
155                reason: format!("expected at least 11 bytes, got {}", section_bytes.len()),
156            });
157        }
158        if section_bytes[4] != 4 {
159            return Err(Error::InvalidSection {
160                section: section_bytes[4],
161                reason: "not a product definition section".into(),
162            });
163        }
164
165        let template = u16::from_be_bytes(section_bytes[7..9].try_into().unwrap());
166        let parameter_category = section_bytes[9];
167        let parameter_number = section_bytes[10];
168
169        Ok(Self {
170            parameter_category,
171            parameter_number,
172            template: ProductDefinitionTemplate::parse(template, section_bytes)?,
173        })
174    }
175
176    pub fn parameter_name(&self, discipline: u8) -> &'static str {
177        parameter::parameter_name(discipline, self.parameter_category, self.parameter_number)
178    }
179
180    pub fn parameter_description(&self, discipline: u8) -> &'static str {
181        parameter::parameter_description(discipline, self.parameter_category, self.parameter_number)
182    }
183
184    pub fn template_number(&self) -> u16 {
185        self.template.number()
186    }
187
188    pub fn generating_process(&self) -> Option<u8> {
189        Some(self.template.base().generating_process)
190    }
191
192    pub fn forecast_time_unit(&self) -> Option<u8> {
193        Some(self.template.base().forecast_time_unit)
194    }
195
196    pub fn forecast_time(&self) -> Option<u32> {
197        Some(self.template.base().forecast_time)
198    }
199
200    pub fn first_surface(&self) -> Option<&FixedSurface> {
201        self.template.base().first_surface.as_ref()
202    }
203
204    pub fn second_surface(&self) -> Option<&FixedSurface> {
205        self.template.base().second_surface.as_ref()
206    }
207
208    pub fn end_of_overall_time_interval(&self) -> Option<ReferenceTime> {
209        self.template.end_of_overall_time_interval()
210    }
211}
212
213impl ProductDefinitionTemplate {
214    pub fn parse(template: u16, section_bytes: &[u8]) -> Result<Self> {
215        match template {
216            0 => Ok(Self::AnalysisOrForecast(AnalysisOrForecastTemplate::parse(
217                section_bytes,
218            )?)),
219            1 => Ok(Self::IndividualEnsembleForecast(
220                IndividualEnsembleForecastTemplate::parse(section_bytes)?,
221            )),
222            8 => Ok(Self::StatisticalProcess(StatisticalProcessTemplate::parse(
223                section_bytes,
224            )?)),
225            11 => Ok(Self::EnsembleStatisticalProcess(
226                EnsembleStatisticalProcessTemplate::parse(section_bytes)?,
227            )),
228            other => Err(Error::UnsupportedProductTemplate(other)),
229        }
230    }
231
232    pub const fn number(&self) -> u16 {
233        match self {
234            Self::AnalysisOrForecast(_) => 0,
235            Self::IndividualEnsembleForecast(_) => 1,
236            Self::StatisticalProcess(_) => 8,
237            Self::EnsembleStatisticalProcess(_) => 11,
238        }
239    }
240
241    fn base(&self) -> &AnalysisOrForecastTemplate {
242        match self {
243            Self::AnalysisOrForecast(template) => template,
244            Self::IndividualEnsembleForecast(template) => &template.base,
245            Self::StatisticalProcess(template) => &template.base,
246            Self::EnsembleStatisticalProcess(template) => &template.ensemble.base,
247        }
248    }
249
250    fn end_of_overall_time_interval(&self) -> Option<ReferenceTime> {
251        match self {
252            Self::StatisticalProcess(template) => Some(template.end_of_overall_time_interval),
253            Self::EnsembleStatisticalProcess(template) => {
254                Some(template.end_of_overall_time_interval)
255            }
256            Self::AnalysisOrForecast(_) | Self::IndividualEnsembleForecast(_) => None,
257        }
258    }
259}
260
261impl AnalysisOrForecastTemplate {
262    const MINIMUM_LENGTH: usize = 34;
263
264    fn parse(section_bytes: &[u8]) -> Result<Self> {
265        require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.0")?;
266
267        Ok(Self {
268            generating_process: section_bytes[11],
269            forecast_time_unit: section_bytes[17],
270            forecast_time: u32::from_be_bytes(section_bytes[18..22].try_into().unwrap()),
271            first_surface: parse_surface(&section_bytes[22..28]),
272            second_surface: parse_surface(&section_bytes[28..34]),
273        })
274    }
275}
276
277impl IndividualEnsembleForecastTemplate {
278    const MINIMUM_LENGTH: usize = 37;
279
280    fn parse(section_bytes: &[u8]) -> Result<Self> {
281        require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.1")?;
282
283        Ok(Self {
284            base: AnalysisOrForecastTemplate::parse(section_bytes)?,
285            type_of_ensemble_forecast: section_bytes[34],
286            perturbation_number: section_bytes[35],
287            number_of_forecasts_in_ensemble: section_bytes[36],
288        })
289    }
290}
291
292impl StatisticalProcessTemplate {
293    const TIME_RANGE_OFFSET: usize = 46;
294
295    fn parse(section_bytes: &[u8]) -> Result<Self> {
296        require_len(section_bytes, Self::TIME_RANGE_OFFSET, "template 4.8")?;
297        let time_range_count = section_bytes[41] as usize;
298        let min_len = required_time_range_template_len(Self::TIME_RANGE_OFFSET, time_range_count)?;
299        require_len(section_bytes, min_len, "template 4.8")?;
300
301        Ok(Self {
302            base: AnalysisOrForecastTemplate::parse(section_bytes)?,
303            end_of_overall_time_interval: parse_reference_time(&section_bytes[34..41], 4)?,
304            number_of_missing_in_statistical_process: u32::from_be_bytes(
305                section_bytes[42..46].try_into().unwrap(),
306            ),
307            time_ranges: parse_statistical_time_ranges(
308                &section_bytes[Self::TIME_RANGE_OFFSET..min_len],
309                time_range_count,
310            ),
311        })
312    }
313}
314
315impl EnsembleStatisticalProcessTemplate {
316    const TIME_RANGE_OFFSET: usize = 49;
317
318    fn parse(section_bytes: &[u8]) -> Result<Self> {
319        require_len(section_bytes, Self::TIME_RANGE_OFFSET, "template 4.11")?;
320        let time_range_count = section_bytes[44] as usize;
321        let min_len = required_time_range_template_len(Self::TIME_RANGE_OFFSET, time_range_count)?;
322        require_len(section_bytes, min_len, "template 4.11")?;
323
324        Ok(Self {
325            ensemble: IndividualEnsembleForecastTemplate::parse(section_bytes)?,
326            end_of_overall_time_interval: parse_reference_time(&section_bytes[37..44], 4)?,
327            number_of_missing_in_statistical_process: u32::from_be_bytes(
328                section_bytes[45..49].try_into().unwrap(),
329            ),
330            time_ranges: parse_statistical_time_ranges(
331                &section_bytes[Self::TIME_RANGE_OFFSET..min_len],
332                time_range_count,
333            ),
334        })
335    }
336}
337
338fn require_len(section_bytes: &[u8], min_len: usize, context: &str) -> Result<()> {
339    if section_bytes.len() < min_len {
340        return Err(Error::InvalidSection {
341            section: 4,
342            reason: format!(
343                "{context} requires at least {min_len} bytes, got {}",
344                section_bytes.len()
345            ),
346        });
347    }
348    Ok(())
349}
350
351fn required_time_range_template_len(
352    time_range_offset: usize,
353    time_range_count: usize,
354) -> Result<usize> {
355    time_range_count
356        .checked_mul(12)
357        .and_then(|len| time_range_offset.checked_add(len))
358        .ok_or_else(|| Error::InvalidSection {
359            section: 4,
360            reason: "statistical time range length overflow".into(),
361        })
362}
363
364fn parse_reference_time(bytes: &[u8], section: u8) -> Result<ReferenceTime> {
365    let reference_time = ReferenceTime {
366        year: u16::from_be_bytes(bytes[0..2].try_into().unwrap()),
367        month: bytes[2],
368        day: bytes[3],
369        hour: bytes[4],
370        minute: bytes[5],
371        second: bytes[6],
372    };
373    reference_time.validate_in_section(section)?;
374    Ok(reference_time)
375}
376
377fn parse_statistical_time_ranges(
378    bytes: &[u8],
379    time_range_count: usize,
380) -> Vec<StatisticalTimeRange> {
381    bytes
382        .chunks_exact(12)
383        .take(time_range_count)
384        .map(|range| StatisticalTimeRange {
385            type_of_statistical_processing: range[0],
386            type_of_time_increment: range[1],
387            time_range_unit: range[2],
388            time_range_length: u32::from_be_bytes(range[3..7].try_into().unwrap()),
389            time_increment_unit: range[7],
390            time_increment: u32::from_be_bytes(range[8..12].try_into().unwrap()),
391        })
392        .collect()
393}
394
395fn parse_surface(section_bytes: &[u8]) -> Option<FixedSurface> {
396    let surface_type = section_bytes[0];
397    if surface_type == 255 {
398        return None;
399    }
400
401    Some(FixedSurface {
402        surface_type,
403        scale_factor: grib_i8(section_bytes[1]),
404        scaled_value: grib_i32(&section_bytes[2..6])?,
405    })
406}
407
408#[cfg(test)]
409mod tests {
410    use super::{
411        AnalysisOrForecastTemplate, Identification, ProductDefinition, ProductDefinitionTemplate,
412    };
413    use crate::error::Error;
414    use crate::metadata::ReferenceTime;
415
416    #[test]
417    fn parses_identification_section() {
418        let section = valid_identification_section();
419
420        let id = Identification::parse(&section).unwrap();
421        assert_eq!(id.center_id, 7);
422        assert_eq!(id.reference_year, 2026);
423        assert_eq!(id.reference_hour, 12);
424    }
425
426    #[test]
427    fn rejects_invalid_identification_reference_time() {
428        let mut section = valid_identification_section();
429        section[14] = 2;
430        section[15] = 29;
431        let err = Identification::parse(&section).unwrap_err();
432        assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
433        assert!(err.to_string().contains("invalid reference timestamp"));
434
435        let mut section = valid_identification_section();
436        section[18] = 60;
437        let err = Identification::parse(&section).unwrap_err();
438        assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
439    }
440
441    #[test]
442    fn parses_product_definition_template_zero_fields() {
443        let section = product_section_template_zero();
444
445        let product = ProductDefinition::parse(&section).unwrap();
446        assert_eq!(product.parameter_category, 2);
447        assert_eq!(product.parameter_number, 3);
448        assert_eq!(product.template_number(), 0);
449        assert_eq!(product.forecast_time(), Some(6));
450        assert_eq!(product.first_surface().unwrap().scaled_value_f64(), 850.0);
451        assert_eq!(
452            product.template,
453            ProductDefinitionTemplate::AnalysisOrForecast(AnalysisOrForecastTemplate {
454                generating_process: 2,
455                forecast_time_unit: 1,
456                forecast_time: 6,
457                first_surface: product.first_surface().cloned(),
458                second_surface: None,
459            })
460        );
461    }
462
463    #[test]
464    fn parses_individual_ensemble_forecast_template() {
465        let mut section = product_section_template_zero();
466        section.resize(37, 0);
467        section[..4].copy_from_slice(&(37u32).to_be_bytes());
468        section[7..9].copy_from_slice(&1u16.to_be_bytes());
469        section[34] = 1;
470        section[35] = 2;
471        section[36] = 20;
472
473        let product = ProductDefinition::parse(&section).unwrap();
474        assert_eq!(product.template_number(), 1);
475        assert_eq!(product.forecast_time(), Some(6));
476        match product.template {
477            ProductDefinitionTemplate::IndividualEnsembleForecast(template) => {
478                assert_eq!(template.type_of_ensemble_forecast, 1);
479                assert_eq!(template.perturbation_number, 2);
480                assert_eq!(template.number_of_forecasts_in_ensemble, 20);
481                assert_eq!(template.base.forecast_time, 6);
482            }
483            other => panic!("expected template 4.1, got {other:?}"),
484        }
485    }
486
487    #[test]
488    fn parses_statistical_process_template() {
489        let section = product_section_template_eight();
490
491        let product = ProductDefinition::parse(&section).unwrap();
492        assert_eq!(product.template_number(), 8);
493        assert_eq!(product.forecast_time(), Some(6));
494        assert_eq!(
495            product.end_of_overall_time_interval(),
496            Some(ReferenceTime {
497                year: 2026,
498                month: 3,
499                day: 20,
500                hour: 18,
501                minute: 0,
502                second: 0,
503            })
504        );
505        match product.template {
506            ProductDefinitionTemplate::StatisticalProcess(template) => {
507                assert_eq!(template.time_ranges.len(), 1);
508                assert_eq!(template.time_ranges[0].type_of_statistical_processing, 1);
509                assert_eq!(template.time_ranges[0].time_range_length, 6);
510            }
511            other => panic!("expected template 4.8, got {other:?}"),
512        }
513    }
514
515    #[test]
516    fn parses_ensemble_statistical_process_template() {
517        let mut section = product_section_template_eight();
518        section.resize(61, 0);
519        section[..4].copy_from_slice(&(61u32).to_be_bytes());
520        section[7..9].copy_from_slice(&11u16.to_be_bytes());
521        section.copy_within(34..58, 37);
522        section[34] = 1;
523        section[35] = 3;
524        section[36] = 20;
525
526        let product = ProductDefinition::parse(&section).unwrap();
527        assert_eq!(product.template_number(), 11);
528        assert_eq!(
529            product.end_of_overall_time_interval(),
530            Some(ReferenceTime {
531                year: 2026,
532                month: 3,
533                day: 20,
534                hour: 18,
535                minute: 0,
536                second: 0,
537            })
538        );
539        match product.template {
540            ProductDefinitionTemplate::EnsembleStatisticalProcess(template) => {
541                assert_eq!(template.ensemble.perturbation_number, 3);
542                assert_eq!(template.time_ranges.len(), 1);
543            }
544            other => panic!("expected template 4.11, got {other:?}"),
545        }
546    }
547
548    #[test]
549    fn rejects_invalid_statistical_process_end_time() {
550        let mut section = product_section_template_eight();
551        section[36] = 2;
552        section[37] = 29;
553
554        let err = ProductDefinition::parse(&section).unwrap_err();
555        assert!(matches!(err, Error::InvalidSection { section: 4, .. }));
556        assert!(err.to_string().contains("invalid reference timestamp"));
557    }
558
559    #[test]
560    fn rejects_unsupported_product_definition_templates() {
561        let mut section = vec![0u8; 34];
562        section[..4].copy_from_slice(&(34u32).to_be_bytes());
563        section[4] = 4;
564        section[7..9].copy_from_slice(&99u16.to_be_bytes());
565        section[9] = 2;
566        section[10] = 3;
567
568        let err = ProductDefinition::parse(&section).unwrap_err();
569        assert!(matches!(err, Error::UnsupportedProductTemplate(99)));
570    }
571
572    #[test]
573    fn rejects_truncated_template_zero_sections() {
574        let mut section = vec![0u8; 33];
575        section[..4].copy_from_slice(&(33u32).to_be_bytes());
576        section[4] = 4;
577        section[7..9].copy_from_slice(&0u16.to_be_bytes());
578        section[9] = 2;
579        section[10] = 3;
580
581        let err = ProductDefinition::parse(&section).unwrap_err();
582        assert!(matches!(err, Error::InvalidSection { section: 4, .. }));
583    }
584
585    fn product_section_template_zero() -> Vec<u8> {
586        let mut section = vec![0u8; 34];
587        section[..4].copy_from_slice(&(34u32).to_be_bytes());
588        section[4] = 4;
589        section[7..9].copy_from_slice(&0u16.to_be_bytes());
590        section[9] = 2;
591        section[10] = 3;
592        section[11] = 2;
593        section[17] = 1;
594        section[18..22].copy_from_slice(&6u32.to_be_bytes());
595        section[22] = 103;
596        section[23] = 0;
597        section[24..28].copy_from_slice(&850u32.to_be_bytes());
598        section[28] = 255;
599        section
600    }
601
602    fn product_section_template_eight() -> Vec<u8> {
603        let mut section = product_section_template_zero();
604        section.resize(58, 0);
605        section[..4].copy_from_slice(&(58u32).to_be_bytes());
606        section[7..9].copy_from_slice(&8u16.to_be_bytes());
607        section[34..36].copy_from_slice(&2026u16.to_be_bytes());
608        section[36] = 3;
609        section[37] = 20;
610        section[38] = 18;
611        section[39] = 0;
612        section[40] = 0;
613        section[41] = 1;
614        section[46] = 1;
615        section[47] = 2;
616        section[48] = 1;
617        section[49..53].copy_from_slice(&6u32.to_be_bytes());
618        section[53] = 255;
619        section
620    }
621
622    fn valid_identification_section() -> Vec<u8> {
623        let mut section = vec![0u8; 21];
624        section[..4].copy_from_slice(&(21u32).to_be_bytes());
625        section[4] = 1;
626        section[5..7].copy_from_slice(&7u16.to_be_bytes());
627        section[7..9].copy_from_slice(&14u16.to_be_bytes());
628        section[9] = 35;
629        section[10] = 1;
630        section[11] = 1;
631        section[12..14].copy_from_slice(&2026u16.to_be_bytes());
632        section[14] = 3;
633        section[15] = 20;
634        section[16] = 12;
635        section[17] = 30;
636        section[18] = 45;
637        section[19] = 0;
638        section[20] = 1;
639        section
640    }
641}