Skip to main content

grib_reader/
product.rs

1//! GRIB2 metadata carried by Sections 1 and 4.
2
3use crate::error::{Error, Result};
4use crate::parameter;
5use crate::util::{grib_i32, grib_i8};
6
7/// Section 1: Identification Section.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Identification {
10    pub center_id: u16,
11    pub subcenter_id: u16,
12    pub master_table_version: u8,
13    pub local_table_version: u8,
14    pub significance_of_reference_time: u8,
15    pub reference_year: u16,
16    pub reference_month: u8,
17    pub reference_day: u8,
18    pub reference_hour: u8,
19    pub reference_minute: u8,
20    pub reference_second: u8,
21    pub production_status: u8,
22    pub processed_data_type: u8,
23}
24
25impl Identification {
26    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
27        if section_bytes.len() < 21 {
28            return Err(Error::InvalidSection {
29                section: 1,
30                reason: format!("expected at least 21 bytes, got {}", section_bytes.len()),
31            });
32        }
33        if section_bytes[4] != 1 {
34            return Err(Error::InvalidSection {
35                section: section_bytes[4],
36                reason: "not an identification section".into(),
37            });
38        }
39
40        Ok(Self {
41            center_id: u16::from_be_bytes(section_bytes[5..7].try_into().unwrap()),
42            subcenter_id: u16::from_be_bytes(section_bytes[7..9].try_into().unwrap()),
43            master_table_version: section_bytes[9],
44            local_table_version: section_bytes[10],
45            significance_of_reference_time: section_bytes[11],
46            reference_year: u16::from_be_bytes(section_bytes[12..14].try_into().unwrap()),
47            reference_month: section_bytes[14],
48            reference_day: section_bytes[15],
49            reference_hour: section_bytes[16],
50            reference_minute: section_bytes[17],
51            reference_second: section_bytes[18],
52            production_status: section_bytes[19],
53            processed_data_type: section_bytes[20],
54        })
55    }
56}
57
58/// A fixed surface from Product Definition templates.
59#[derive(Debug, Clone, PartialEq)]
60pub struct FixedSurface {
61    pub surface_type: u8,
62    pub scale_factor: i16,
63    pub scaled_value: i32,
64}
65
66impl FixedSurface {
67    pub fn scaled_value_f64(&self) -> f64 {
68        let factor = 10.0_f64.powi(-(self.scale_factor as i32));
69        self.scaled_value as f64 * factor
70    }
71}
72
73/// Section 4: Product Definition Section.
74#[derive(Debug, Clone, PartialEq)]
75pub struct ProductDefinition {
76    pub template: u16,
77    pub parameter_category: u8,
78    pub parameter_number: u8,
79    pub generating_process: Option<u8>,
80    pub forecast_time_unit: Option<u8>,
81    pub forecast_time: Option<u32>,
82    pub first_surface: Option<FixedSurface>,
83    pub second_surface: Option<FixedSurface>,
84}
85
86impl ProductDefinition {
87    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
88        if section_bytes.len() < 11 {
89            return Err(Error::InvalidSection {
90                section: 4,
91                reason: format!("expected at least 11 bytes, got {}", section_bytes.len()),
92            });
93        }
94        if section_bytes[4] != 4 {
95            return Err(Error::InvalidSection {
96                section: section_bytes[4],
97                reason: "not a product definition section".into(),
98            });
99        }
100
101        let template = u16::from_be_bytes(section_bytes[7..9].try_into().unwrap());
102        let parameter_category = section_bytes[9];
103        let parameter_number = section_bytes[10];
104
105        let generating_process = section_bytes.get(11).copied();
106        let forecast_time_unit = section_bytes.get(17).copied();
107        let forecast_time = (section_bytes.len() >= 22)
108            .then(|| u32::from_be_bytes(section_bytes[18..22].try_into().unwrap()));
109        let first_surface = parse_surface(section_bytes, 22);
110        let second_surface = parse_surface(section_bytes, 28);
111
112        Ok(Self {
113            template,
114            parameter_category,
115            parameter_number,
116            generating_process,
117            forecast_time_unit,
118            forecast_time,
119            first_surface,
120            second_surface,
121        })
122    }
123
124    pub fn parameter_name(&self, discipline: u8) -> &'static str {
125        parameter::parameter_name(discipline, self.parameter_category, self.parameter_number)
126    }
127
128    pub fn parameter_description(&self, discipline: u8) -> &'static str {
129        parameter::parameter_description(discipline, self.parameter_category, self.parameter_number)
130    }
131}
132
133fn parse_surface(section_bytes: &[u8], offset: usize) -> Option<FixedSurface> {
134    if section_bytes.len() < offset + 6 {
135        return None;
136    }
137
138    let surface_type = section_bytes[offset];
139    if surface_type == 255 {
140        return None;
141    }
142
143    Some(FixedSurface {
144        surface_type,
145        scale_factor: grib_i8(section_bytes[offset + 1]),
146        scaled_value: grib_i32(&section_bytes[offset + 2..offset + 6])?,
147    })
148}
149
150#[cfg(test)]
151mod tests {
152    use super::{Identification, ProductDefinition};
153
154    #[test]
155    fn parses_identification_section() {
156        let mut section = vec![0u8; 21];
157        section[..4].copy_from_slice(&(21u32).to_be_bytes());
158        section[4] = 1;
159        section[5..7].copy_from_slice(&7u16.to_be_bytes());
160        section[7..9].copy_from_slice(&14u16.to_be_bytes());
161        section[9] = 35;
162        section[10] = 1;
163        section[11] = 1;
164        section[12..14].copy_from_slice(&2026u16.to_be_bytes());
165        section[14] = 3;
166        section[15] = 20;
167        section[16] = 12;
168        section[17] = 30;
169        section[18] = 45;
170        section[19] = 0;
171        section[20] = 1;
172
173        let id = Identification::parse(&section).unwrap();
174        assert_eq!(id.center_id, 7);
175        assert_eq!(id.reference_year, 2026);
176        assert_eq!(id.reference_hour, 12);
177    }
178
179    #[test]
180    fn parses_product_definition_template_zero_fields() {
181        let mut section = vec![0u8; 34];
182        section[..4].copy_from_slice(&(34u32).to_be_bytes());
183        section[4] = 4;
184        section[7..9].copy_from_slice(&0u16.to_be_bytes());
185        section[9] = 2;
186        section[10] = 3;
187        section[11] = 2;
188        section[17] = 1;
189        section[18..22].copy_from_slice(&6u32.to_be_bytes());
190        section[22] = 103;
191        section[23] = 0;
192        section[24..28].copy_from_slice(&850u32.to_be_bytes());
193
194        let product = ProductDefinition::parse(&section).unwrap();
195        assert_eq!(product.template, 0);
196        assert_eq!(product.parameter_category, 2);
197        assert_eq!(product.parameter_number, 3);
198        assert_eq!(product.forecast_time, Some(6));
199        assert_eq!(product.first_surface.unwrap().scaled_value_f64(), 850.0);
200    }
201}