1use crate::error::{Error, Result};
4use crate::parameter;
5use crate::util::{grib_i32, grib_i8};
6
7#[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#[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#[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(§ion_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(§ion).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(§ion).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}