1use crate::binary::{decode_wmo_i32, decode_wmo_i8};
4use crate::error::{Error, Result};
5use crate::metadata::ReferenceTime;
6use crate::parameter;
7
8#[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#[derive(Debug, Clone, PartialEq)]
71pub struct FixedSurface {
72 pub surface_type: u8,
73 pub value: Option<ScaledValue>,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct ScaledValue {
81 pub scale_factor: i16,
82 pub scaled_value: i32,
83}
84
85impl FixedSurface {
86 pub const fn with_value(surface_type: u8, scale_factor: i16, scaled_value: i32) -> Self {
87 Self {
88 surface_type,
89 value: Some(ScaledValue {
90 scale_factor,
91 scaled_value,
92 }),
93 }
94 }
95
96 pub const fn without_value(surface_type: u8) -> Self {
97 Self {
98 surface_type,
99 value: None,
100 }
101 }
102
103 pub fn scaled_value_f64(&self) -> Option<f64> {
104 let value = self.value?;
105 let factor = 10.0_f64.powi(-i32::from(value.scale_factor));
106 Some(f64::from(value.scaled_value) * factor)
107 }
108}
109
110#[derive(Debug, Clone, PartialEq)]
112pub struct ProductDefinition {
113 pub parameter_category: u8,
114 pub parameter_number: u8,
115 pub template: ProductDefinitionTemplate,
116}
117
118#[derive(Debug, Clone, PartialEq)]
120#[non_exhaustive]
121pub enum ProductDefinitionTemplate {
122 AnalysisOrForecast(AnalysisOrForecastTemplate),
123 IndividualEnsembleForecast(IndividualEnsembleForecastTemplate),
124 DerivedForecast(DerivedForecastTemplate),
125 ProbabilityForecast(ProbabilityForecastTemplate),
126 PercentileForecast(PercentileForecastTemplate),
127 StatisticalProcess(StatisticalProcessTemplate),
128 ProbabilityStatisticalProcess(ProbabilityStatisticalProcessTemplate),
129 PercentileStatisticalProcess(PercentileStatisticalProcessTemplate),
130 EnsembleStatisticalProcess(EnsembleStatisticalProcessTemplate),
131 DerivedStatisticalProcess(DerivedStatisticalProcessTemplate),
132 SpatialProcess(SpatialProcessTemplate),
133 Unsupported {
137 number: u16,
138 raw: Vec<u8>,
139 },
140}
141
142#[derive(Debug, Clone, PartialEq)]
144pub struct AnalysisOrForecastTemplate {
145 pub type_of_generating_process: u8,
146 pub background_generating_process_identifier: u8,
147 pub generating_process_identifier: u8,
148 pub hours_after_data_cutoff: Option<u16>,
149 pub minutes_after_data_cutoff: Option<u8>,
150 pub forecast_time_unit: u8,
151 pub forecast_time: i32,
152 pub first_surface: Option<FixedSurface>,
153 pub second_surface: Option<FixedSurface>,
154}
155
156#[derive(Debug, Clone, PartialEq)]
158pub struct IndividualEnsembleForecastTemplate {
159 pub base: AnalysisOrForecastTemplate,
160 pub type_of_ensemble_forecast: u8,
161 pub perturbation_number: u8,
162 pub number_of_forecasts_in_ensemble: u8,
163}
164
165#[derive(Debug, Clone, PartialEq)]
167pub struct DerivedForecastTemplate {
168 pub base: AnalysisOrForecastTemplate,
169 pub derived_forecast_type: u8,
170 pub number_of_forecasts_in_ensemble: u8,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct ProbabilityLimit {
176 pub scale_factor: i16,
177 pub scaled_value: i32,
178}
179
180impl ProbabilityLimit {
181 pub fn value_f64(self) -> f64 {
182 f64::from(self.scaled_value) * 10.0_f64.powi(-i32::from(self.scale_factor))
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193#[non_exhaustive]
194pub enum ProbabilityType {
195 BelowLowerLimit(ProbabilityLimit),
196 AboveUpperLimit(ProbabilityLimit),
197 BetweenLimits {
198 lower: ProbabilityLimit,
199 upper: ProbabilityLimit,
200 },
201 AboveLowerLimit(ProbabilityLimit),
202 BelowUpperLimit(ProbabilityLimit),
203 EqualLowerLimit(ProbabilityLimit),
204 AboveNormal,
205 NearNormal,
206 BelowNormal,
207 CategoricalBoolean,
208 Quantile,
209 Missing,
210 Other {
211 code: u8,
212 lower: Option<ProbabilityLimit>,
213 upper: Option<ProbabilityLimit>,
214 },
215}
216
217impl ProbabilityType {
218 pub const fn code(self) -> u8 {
219 match self {
220 Self::BelowLowerLimit(_) => 0,
221 Self::AboveUpperLimit(_) => 1,
222 Self::BetweenLimits { .. } => 2,
223 Self::AboveLowerLimit(_) => 3,
224 Self::BelowUpperLimit(_) => 4,
225 Self::EqualLowerLimit(_) => 5,
226 Self::AboveNormal => 6,
227 Self::NearNormal => 7,
228 Self::BelowNormal => 8,
229 Self::CategoricalBoolean => 9,
230 Self::Quantile => 10,
231 Self::Missing => 255,
232 Self::Other { code, .. } => code,
233 }
234 }
235
236 pub const fn lower_limit(self) -> Option<ProbabilityLimit> {
237 match self {
238 Self::BelowLowerLimit(limit)
239 | Self::AboveLowerLimit(limit)
240 | Self::EqualLowerLimit(limit) => Some(limit),
241 Self::BetweenLimits { lower, .. } => Some(lower),
242 Self::Other { lower, .. } => lower,
243 _ => None,
244 }
245 }
246
247 pub const fn upper_limit(self) -> Option<ProbabilityLimit> {
248 match self {
249 Self::AboveUpperLimit(limit) | Self::BelowUpperLimit(limit) => Some(limit),
250 Self::BetweenLimits { upper, .. } => Some(upper),
251 Self::Other { upper, .. } => upper,
252 _ => None,
253 }
254 }
255
256 fn from_code_and_limits(
257 code: u8,
258 lower: Option<ProbabilityLimit>,
259 upper: Option<ProbabilityLimit>,
260 ) -> Self {
261 match (code, lower, upper) {
262 (0, Some(limit), None) => Self::BelowLowerLimit(limit),
263 (1, None, Some(limit)) => Self::AboveUpperLimit(limit),
264 (2, Some(lower), Some(upper)) => Self::BetweenLimits { lower, upper },
265 (3, Some(limit), None) => Self::AboveLowerLimit(limit),
266 (4, None, Some(limit)) => Self::BelowUpperLimit(limit),
267 (5, Some(limit), None) => Self::EqualLowerLimit(limit),
268 (6, None, None) => Self::AboveNormal,
269 (7, None, None) => Self::NearNormal,
270 (8, None, None) => Self::BelowNormal,
271 (9, None, None) => Self::CategoricalBoolean,
272 (10, None, None) => Self::Quantile,
273 (255, None, None) => Self::Missing,
274 (code, lower, upper) => Self::Other { code, lower, upper },
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq)]
281pub struct ProbabilityForecastTemplate {
282 pub base: AnalysisOrForecastTemplate,
283 pub forecast_probability_number: u8,
284 pub total_number_of_forecast_probabilities: u8,
285 pub probability: ProbabilityType,
286}
287
288#[derive(Debug, Clone, PartialEq)]
290pub struct PercentileForecastTemplate {
291 pub base: AnalysisOrForecastTemplate,
292 pub percentile_value: u8,
293}
294
295#[derive(Debug, Clone, PartialEq)]
297pub struct StatisticalInterval {
298 pub end_of_overall_time_interval: ReferenceTime,
299 pub number_of_missing_in_statistical_process: u32,
300 pub time_ranges: Vec<StatisticalTimeRange>,
301}
302
303#[derive(Debug, Clone, PartialEq)]
305pub struct StatisticalProcessTemplate {
306 pub base: AnalysisOrForecastTemplate,
307 pub interval: StatisticalInterval,
308}
309
310#[derive(Debug, Clone, PartialEq)]
312pub struct ProbabilityStatisticalProcessTemplate {
313 pub probability: ProbabilityForecastTemplate,
314 pub interval: StatisticalInterval,
315}
316
317#[derive(Debug, Clone, PartialEq)]
319pub struct PercentileStatisticalProcessTemplate {
320 pub percentile: PercentileForecastTemplate,
321 pub interval: StatisticalInterval,
322}
323
324#[derive(Debug, Clone, PartialEq)]
326pub struct EnsembleStatisticalProcessTemplate {
327 pub ensemble: IndividualEnsembleForecastTemplate,
328 pub interval: StatisticalInterval,
329}
330
331#[derive(Debug, Clone, PartialEq)]
333pub struct DerivedStatisticalProcessTemplate {
334 pub derived: DerivedForecastTemplate,
335 pub interval: StatisticalInterval,
336}
337
338#[derive(Debug, Clone, PartialEq)]
340pub struct SpatialProcessTemplate {
341 pub base: AnalysisOrForecastTemplate,
342 pub statistical_process: u8,
343 pub spatial_processing: u8,
344 pub number_of_points_used: u8,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct StatisticalTimeRange {
351 pub type_of_statistical_processing: u8,
352 pub type_of_time_increment: u8,
353 pub time_range_unit: u8,
354 pub time_range_length: u32,
355 pub time_increment_unit: u8,
356 pub time_increment: u32,
357}
358
359impl ProductDefinition {
360 pub fn parse(section_bytes: &[u8]) -> Result<Self> {
361 if section_bytes.len() < 11 {
362 return Err(Error::InvalidSection {
363 section: 4,
364 reason: format!("expected at least 11 bytes, got {}", section_bytes.len()),
365 });
366 }
367 if section_bytes[4] != 4 {
368 return Err(Error::InvalidSection {
369 section: section_bytes[4],
370 reason: "not a product definition section".into(),
371 });
372 }
373
374 let template = u16::from_be_bytes(section_bytes[7..9].try_into().unwrap());
375 let parameter_category = section_bytes[9];
376 let parameter_number = section_bytes[10];
377
378 Ok(Self {
379 parameter_category,
380 parameter_number,
381 template: ProductDefinitionTemplate::parse(template, section_bytes)?,
382 })
383 }
384
385 pub fn parameter_name(&self, discipline: u8) -> &'static str {
386 parameter::parameter_name(discipline, self.parameter_category, self.parameter_number)
387 }
388
389 pub fn parameter_description(&self, discipline: u8) -> &'static str {
390 parameter::parameter_description(discipline, self.parameter_category, self.parameter_number)
391 }
392
393 pub fn template_number(&self) -> u16 {
394 self.template.number()
395 }
396
397 pub fn type_of_generating_process(&self) -> Option<u8> {
398 self.template
399 .base()
400 .map(|base| base.type_of_generating_process)
401 }
402
403 pub fn generating_process_identifier(&self) -> Option<u8> {
404 self.template
405 .base()
406 .map(|base| base.generating_process_identifier)
407 }
408
409 pub fn forecast_time_unit(&self) -> Option<u8> {
410 self.template.base().map(|base| base.forecast_time_unit)
411 }
412
413 pub fn forecast_time(&self) -> Option<i32> {
414 self.template.base().map(|base| base.forecast_time)
415 }
416
417 pub fn first_surface(&self) -> Option<&FixedSurface> {
418 self.template
419 .base()
420 .and_then(|base| base.first_surface.as_ref())
421 }
422
423 pub fn second_surface(&self) -> Option<&FixedSurface> {
424 self.template
425 .base()
426 .and_then(|base| base.second_surface.as_ref())
427 }
428
429 pub fn end_of_overall_time_interval(&self) -> Option<ReferenceTime> {
430 self.template.end_of_overall_time_interval()
431 }
432}
433
434impl ProductDefinitionTemplate {
435 pub fn parse(template: u16, section_bytes: &[u8]) -> Result<Self> {
436 match template {
437 0 => Ok(Self::AnalysisOrForecast(AnalysisOrForecastTemplate::parse(
438 section_bytes,
439 )?)),
440 1 => Ok(Self::IndividualEnsembleForecast(
441 IndividualEnsembleForecastTemplate::parse(section_bytes)?,
442 )),
443 2 => Ok(Self::DerivedForecast(DerivedForecastTemplate::parse(
444 section_bytes,
445 )?)),
446 5 => Ok(Self::ProbabilityForecast(
447 ProbabilityForecastTemplate::parse(section_bytes)?,
448 )),
449 6 => Ok(Self::PercentileForecast(PercentileForecastTemplate::parse(
450 section_bytes,
451 )?)),
452 8 => Ok(Self::StatisticalProcess(StatisticalProcessTemplate::parse(
453 section_bytes,
454 )?)),
455 9 => Ok(Self::ProbabilityStatisticalProcess(
456 ProbabilityStatisticalProcessTemplate::parse(section_bytes)?,
457 )),
458 10 => Ok(Self::PercentileStatisticalProcess(
459 PercentileStatisticalProcessTemplate::parse(section_bytes)?,
460 )),
461 11 => Ok(Self::EnsembleStatisticalProcess(
462 EnsembleStatisticalProcessTemplate::parse(section_bytes)?,
463 )),
464 12 => Ok(Self::DerivedStatisticalProcess(
465 DerivedStatisticalProcessTemplate::parse(section_bytes)?,
466 )),
467 15 => Ok(Self::SpatialProcess(SpatialProcessTemplate::parse(
468 section_bytes,
469 )?)),
470 number => {
471 let raw_len = section_bytes.len() - 11;
472 let mut raw = Vec::new();
473 raw.try_reserve(raw_len).map_err(|error| {
474 Error::allocation("unsupported product-template bytes", raw_len, error)
475 })?;
476 raw.extend_from_slice(§ion_bytes[11..]);
477 Ok(Self::Unsupported { number, raw })
478 }
479 }
480 }
481
482 pub const fn number(&self) -> u16 {
483 match self {
484 Self::AnalysisOrForecast(_) => 0,
485 Self::IndividualEnsembleForecast(_) => 1,
486 Self::DerivedForecast(_) => 2,
487 Self::ProbabilityForecast(_) => 5,
488 Self::PercentileForecast(_) => 6,
489 Self::StatisticalProcess(_) => 8,
490 Self::ProbabilityStatisticalProcess(_) => 9,
491 Self::PercentileStatisticalProcess(_) => 10,
492 Self::EnsembleStatisticalProcess(_) => 11,
493 Self::DerivedStatisticalProcess(_) => 12,
494 Self::SpatialProcess(_) => 15,
495 Self::Unsupported { number, .. } => *number,
496 }
497 }
498
499 fn base(&self) -> Option<&AnalysisOrForecastTemplate> {
500 Some(match self {
501 Self::AnalysisOrForecast(template) => template,
502 Self::IndividualEnsembleForecast(template) => &template.base,
503 Self::DerivedForecast(template) => &template.base,
504 Self::ProbabilityForecast(template) => &template.base,
505 Self::PercentileForecast(template) => &template.base,
506 Self::StatisticalProcess(template) => &template.base,
507 Self::ProbabilityStatisticalProcess(template) => &template.probability.base,
508 Self::PercentileStatisticalProcess(template) => &template.percentile.base,
509 Self::EnsembleStatisticalProcess(template) => &template.ensemble.base,
510 Self::DerivedStatisticalProcess(template) => &template.derived.base,
511 Self::SpatialProcess(template) => &template.base,
512 Self::Unsupported { .. } => return None,
513 })
514 }
515
516 fn end_of_overall_time_interval(&self) -> Option<ReferenceTime> {
517 match self {
518 Self::StatisticalProcess(template) => {
519 Some(template.interval.end_of_overall_time_interval)
520 }
521 Self::ProbabilityStatisticalProcess(template) => {
522 Some(template.interval.end_of_overall_time_interval)
523 }
524 Self::PercentileStatisticalProcess(template) => {
525 Some(template.interval.end_of_overall_time_interval)
526 }
527 Self::EnsembleStatisticalProcess(template) => {
528 Some(template.interval.end_of_overall_time_interval)
529 }
530 Self::DerivedStatisticalProcess(template) => {
531 Some(template.interval.end_of_overall_time_interval)
532 }
533 Self::AnalysisOrForecast(_)
534 | Self::IndividualEnsembleForecast(_)
535 | Self::DerivedForecast(_)
536 | Self::ProbabilityForecast(_)
537 | Self::PercentileForecast(_)
538 | Self::SpatialProcess(_) => None,
539 Self::Unsupported { .. } => None,
540 }
541 }
542}
543
544impl AnalysisOrForecastTemplate {
545 const MINIMUM_LENGTH: usize = 34;
546
547 fn parse(section_bytes: &[u8]) -> Result<Self> {
548 require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.0")?;
549
550 let minutes_after_data_cutoff = (section_bytes[16] != 0xff).then_some(section_bytes[16]);
551 if minutes_after_data_cutoff.is_some_and(|minutes| minutes > 59) {
552 return Err(Error::InvalidSection {
553 section: 4,
554 reason: "minutes after data cutoff must be at most 59".into(),
555 });
556 }
557
558 Ok(Self {
559 type_of_generating_process: section_bytes[11],
560 background_generating_process_identifier: section_bytes[12],
561 generating_process_identifier: section_bytes[13],
562 hours_after_data_cutoff: (section_bytes[14..16] != [0xff; 2])
563 .then(|| u16::from_be_bytes(section_bytes[14..16].try_into().unwrap())),
564 minutes_after_data_cutoff,
565 forecast_time_unit: section_bytes[17],
566 forecast_time: decode_wmo_i32(§ion_bytes[18..22]).unwrap(),
567 first_surface: parse_surface(§ion_bytes[22..28]),
568 second_surface: parse_surface(§ion_bytes[28..34]),
569 })
570 }
571}
572
573impl IndividualEnsembleForecastTemplate {
574 const MINIMUM_LENGTH: usize = 37;
575
576 fn parse(section_bytes: &[u8]) -> Result<Self> {
577 require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.1")?;
578
579 Ok(Self {
580 base: AnalysisOrForecastTemplate::parse(section_bytes)?,
581 type_of_ensemble_forecast: section_bytes[34],
582 perturbation_number: section_bytes[35],
583 number_of_forecasts_in_ensemble: section_bytes[36],
584 })
585 }
586}
587
588impl DerivedForecastTemplate {
589 const MINIMUM_LENGTH: usize = 36;
590
591 fn parse(section_bytes: &[u8]) -> Result<Self> {
592 require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.2")?;
593
594 Ok(Self {
595 base: AnalysisOrForecastTemplate::parse(section_bytes)?,
596 derived_forecast_type: section_bytes[34],
597 number_of_forecasts_in_ensemble: section_bytes[35],
598 })
599 }
600}
601
602impl ProbabilityForecastTemplate {
603 const MINIMUM_LENGTH: usize = 47;
604
605 fn parse(section_bytes: &[u8]) -> Result<Self> {
606 require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.5")?;
607
608 let lower = parse_probability_limit(§ion_bytes[37..42]);
609 let upper = parse_probability_limit(§ion_bytes[42..47]);
610 Ok(Self {
611 base: AnalysisOrForecastTemplate::parse(section_bytes)?,
612 forecast_probability_number: section_bytes[34],
613 total_number_of_forecast_probabilities: section_bytes[35],
614 probability: ProbabilityType::from_code_and_limits(section_bytes[36], lower, upper),
615 })
616 }
617}
618
619impl PercentileForecastTemplate {
620 const MINIMUM_LENGTH: usize = 35;
621
622 fn parse(section_bytes: &[u8]) -> Result<Self> {
623 require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.6")?;
624
625 let percentile_value = section_bytes[34];
626 if percentile_value > 100 {
627 return Err(Error::InvalidSection {
628 section: 4,
629 reason: format!("template 4.6 percentile {percentile_value} exceeds 100"),
630 });
631 }
632 Ok(Self {
633 base: AnalysisOrForecastTemplate::parse(section_bytes)?,
634 percentile_value,
635 })
636 }
637}
638
639impl StatisticalProcessTemplate {
640 fn parse(section_bytes: &[u8]) -> Result<Self> {
641 Ok(Self {
642 base: AnalysisOrForecastTemplate::parse(section_bytes)?,
643 interval: StatisticalInterval::parse(section_bytes, 34, "template 4.8")?,
644 })
645 }
646}
647
648impl ProbabilityStatisticalProcessTemplate {
649 fn parse(section_bytes: &[u8]) -> Result<Self> {
650 Ok(Self {
651 probability: ProbabilityForecastTemplate::parse(section_bytes)?,
652 interval: StatisticalInterval::parse(section_bytes, 47, "template 4.9")?,
653 })
654 }
655}
656
657impl PercentileStatisticalProcessTemplate {
658 fn parse(section_bytes: &[u8]) -> Result<Self> {
659 Ok(Self {
660 percentile: PercentileForecastTemplate::parse(section_bytes)?,
661 interval: StatisticalInterval::parse(section_bytes, 35, "template 4.10")?,
662 })
663 }
664}
665
666impl EnsembleStatisticalProcessTemplate {
667 fn parse(section_bytes: &[u8]) -> Result<Self> {
668 Ok(Self {
669 ensemble: IndividualEnsembleForecastTemplate::parse(section_bytes)?,
670 interval: StatisticalInterval::parse(section_bytes, 37, "template 4.11")?,
671 })
672 }
673}
674
675impl DerivedStatisticalProcessTemplate {
676 fn parse(section_bytes: &[u8]) -> Result<Self> {
677 Ok(Self {
678 derived: DerivedForecastTemplate::parse(section_bytes)?,
679 interval: StatisticalInterval::parse(section_bytes, 36, "template 4.12")?,
680 })
681 }
682}
683
684impl SpatialProcessTemplate {
685 const MINIMUM_LENGTH: usize = 37;
686
687 fn parse(section_bytes: &[u8]) -> Result<Self> {
688 require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.15")?;
689
690 Ok(Self {
691 base: AnalysisOrForecastTemplate::parse(section_bytes)?,
692 statistical_process: section_bytes[34],
693 spatial_processing: section_bytes[35],
694 number_of_points_used: section_bytes[36],
695 })
696 }
697}
698
699impl StatisticalInterval {
700 fn parse(section_bytes: &[u8], end_time_offset: usize, context: &str) -> Result<Self> {
701 let time_range_offset =
702 end_time_offset
703 .checked_add(12)
704 .ok_or_else(|| Error::InvalidSection {
705 section: 4,
706 reason: "statistical interval offset overflow".into(),
707 })?;
708 require_len(section_bytes, time_range_offset, context)?;
709 let time_range_count = section_bytes[end_time_offset + 7] as usize;
710 let min_len = required_time_range_template_len(time_range_offset, time_range_count)?;
711 require_len(section_bytes, min_len, context)?;
712
713 Ok(Self {
714 end_of_overall_time_interval: parse_reference_time(
715 §ion_bytes[end_time_offset..end_time_offset + 7],
716 4,
717 )?,
718 number_of_missing_in_statistical_process: u32::from_be_bytes(
719 section_bytes[end_time_offset + 8..end_time_offset + 12]
720 .try_into()
721 .unwrap(),
722 ),
723 time_ranges: parse_statistical_time_ranges(
724 §ion_bytes[time_range_offset..min_len],
725 time_range_count,
726 )?,
727 })
728 }
729}
730
731fn require_len(section_bytes: &[u8], min_len: usize, context: &str) -> Result<()> {
732 if section_bytes.len() < min_len {
733 return Err(Error::InvalidSection {
734 section: 4,
735 reason: format!(
736 "{context} requires at least {min_len} bytes, got {}",
737 section_bytes.len()
738 ),
739 });
740 }
741 Ok(())
742}
743
744fn required_time_range_template_len(
745 time_range_offset: usize,
746 time_range_count: usize,
747) -> Result<usize> {
748 time_range_count
749 .checked_mul(12)
750 .and_then(|len| time_range_offset.checked_add(len))
751 .ok_or_else(|| Error::InvalidSection {
752 section: 4,
753 reason: "statistical time range length overflow".into(),
754 })
755}
756
757fn parse_reference_time(bytes: &[u8], section: u8) -> Result<ReferenceTime> {
758 let reference_time = ReferenceTime {
759 year: u16::from_be_bytes(bytes[0..2].try_into().unwrap()),
760 month: bytes[2],
761 day: bytes[3],
762 hour: bytes[4],
763 minute: bytes[5],
764 second: bytes[6],
765 };
766 reference_time.validate_in_section(section)?;
767 Ok(reference_time)
768}
769
770fn parse_statistical_time_ranges(
771 bytes: &[u8],
772 time_range_count: usize,
773) -> Result<Vec<StatisticalTimeRange>> {
774 let mut ranges = Vec::new();
775 ranges
776 .try_reserve_exact(time_range_count)
777 .map_err(|error| {
778 Error::allocation(
779 "statistical time-range descriptors",
780 time_range_count,
781 error,
782 )
783 })?;
784 for range in bytes.chunks_exact(12).take(time_range_count) {
785 ranges.push(StatisticalTimeRange {
786 type_of_statistical_processing: range[0],
787 type_of_time_increment: range[1],
788 time_range_unit: range[2],
789 time_range_length: u32::from_be_bytes(range[3..7].try_into().unwrap()),
790 time_increment_unit: range[7],
791 time_increment: u32::from_be_bytes(range[8..12].try_into().unwrap()),
792 });
793 }
794 Ok(ranges)
795}
796
797fn parse_probability_limit(bytes: &[u8]) -> Option<ProbabilityLimit> {
798 if bytes[0] == 0xff || bytes[1..5] == [0xff; 4] {
799 return None;
800 }
801
802 Some(ProbabilityLimit {
803 scale_factor: decode_wmo_i8(bytes[0]),
804 scaled_value: decode_wmo_i32(&bytes[1..5])?,
805 })
806}
807
808fn parse_surface(section_bytes: &[u8]) -> Option<FixedSurface> {
809 let surface_type = section_bytes[0];
810 if surface_type == 255 {
811 return None;
812 }
813
814 let value = if section_bytes[1] == 0xff || section_bytes[2..6] == [0xff; 4] {
815 None
816 } else {
817 Some(ScaledValue {
818 scale_factor: decode_wmo_i8(section_bytes[1]),
819 scaled_value: decode_wmo_i32(§ion_bytes[2..6])?,
820 })
821 };
822
823 Some(FixedSurface {
824 surface_type,
825 value,
826 })
827}
828
829#[cfg(test)]
830mod tests {
831 use super::{
832 AnalysisOrForecastTemplate, Identification, ProbabilityType, ProductDefinition,
833 ProductDefinitionTemplate,
834 };
835 use crate::error::Error;
836 use crate::metadata::ReferenceTime;
837
838 #[test]
839 fn parses_identification_section() {
840 let section = valid_identification_section();
841
842 let id = Identification::parse(§ion).unwrap();
843 assert_eq!(id.center_id, 7);
844 assert_eq!(id.reference_year, 2026);
845 assert_eq!(id.reference_hour, 12);
846 }
847
848 #[test]
849 fn rejects_invalid_identification_reference_time() {
850 let mut section = valid_identification_section();
851 section[14] = 2;
852 section[15] = 29;
853 let err = Identification::parse(§ion).unwrap_err();
854 assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
855 assert!(err.to_string().contains("invalid reference timestamp"));
856
857 let mut section = valid_identification_section();
858 section[18] = 60;
859 let err = Identification::parse(§ion).unwrap_err();
860 assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
861 }
862
863 #[test]
864 fn parses_product_definition_template_zero_fields() {
865 let section = product_section_template_zero();
866
867 let product = ProductDefinition::parse(§ion).unwrap();
868 assert_eq!(product.parameter_category, 2);
869 assert_eq!(product.parameter_number, 3);
870 assert_eq!(product.template_number(), 0);
871 assert_eq!(product.forecast_time(), Some(6));
872 assert_eq!(
873 product.first_surface().unwrap().scaled_value_f64(),
874 Some(850.0)
875 );
876 assert_eq!(
877 product.template,
878 ProductDefinitionTemplate::AnalysisOrForecast(AnalysisOrForecastTemplate {
879 type_of_generating_process: 2,
880 background_generating_process_identifier: 0,
881 generating_process_identifier: 0,
882 hours_after_data_cutoff: Some(0),
883 minutes_after_data_cutoff: Some(0),
884 forecast_time_unit: 1,
885 forecast_time: 6,
886 first_surface: product.first_surface().cloned(),
887 second_surface: None,
888 })
889 );
890 }
891
892 #[test]
893 fn parses_individual_ensemble_forecast_template() {
894 let mut section = product_section_template_zero();
895 section.resize(37, 0);
896 section[..4].copy_from_slice(&(37u32).to_be_bytes());
897 section[7..9].copy_from_slice(&1u16.to_be_bytes());
898 section[34] = 1;
899 section[35] = 2;
900 section[36] = 20;
901
902 let product = ProductDefinition::parse(§ion).unwrap();
903 assert_eq!(product.template_number(), 1);
904 assert_eq!(product.forecast_time(), Some(6));
905 match product.template {
906 ProductDefinitionTemplate::IndividualEnsembleForecast(template) => {
907 assert_eq!(template.type_of_ensemble_forecast, 1);
908 assert_eq!(template.perturbation_number, 2);
909 assert_eq!(template.number_of_forecasts_in_ensemble, 20);
910 assert_eq!(template.base.forecast_time, 6);
911 }
912 other => panic!("expected template 4.1, got {other:?}"),
913 }
914 }
915
916 #[test]
917 fn parses_derived_forecast_template() {
918 let mut section = product_section_template_zero();
919 section.resize(36, 0);
920 set_product_template(&mut section, 2);
921 section[34] = 4;
922 section[35] = 50;
923
924 let product = ProductDefinition::parse(§ion).unwrap();
925 let ProductDefinitionTemplate::DerivedForecast(template) = product.template else {
926 panic!("expected template 4.2");
927 };
928 assert_eq!(template.derived_forecast_type, 4);
929 assert_eq!(template.number_of_forecasts_in_ensemble, 50);
930 assert_eq!(template.base.forecast_time, 6);
931 }
932
933 #[test]
934 fn parses_probability_forecast_with_typed_thresholds() {
935 let mut section = product_section_template_zero();
936 section.resize(47, 0xff);
937 set_product_template(&mut section, 5);
938 section[34] = 2;
939 section[35] = 10;
940 section[36] = 2;
941 section[37] = 1;
942 section[38..42].copy_from_slice(&crate::binary::encode_wmo_i32(-125).unwrap());
943 section[42] = 1;
944 section[43..47].copy_from_slice(&crate::binary::encode_wmo_i32(250).unwrap());
945
946 let product = ProductDefinition::parse(§ion).unwrap();
947 let ProductDefinitionTemplate::ProbabilityForecast(template) = product.template else {
948 panic!("expected template 4.5");
949 };
950 assert_eq!(template.forecast_probability_number, 2);
951 assert_eq!(template.total_number_of_forecast_probabilities, 10);
952 let ProbabilityType::BetweenLimits { lower, upper } = template.probability else {
953 panic!("expected a between-limits probability");
954 };
955 assert_eq!(lower.value_f64(), -12.5);
956 assert_eq!(upper.value_f64(), 25.0);
957 }
958
959 #[test]
960 fn preserves_noncanonical_probability_limit_combinations() {
961 let mut section = product_section_template_zero();
962 section.resize(47, 0xff);
963 set_product_template(&mut section, 5);
964 section[36] = 0;
965
966 let product = ProductDefinition::parse(§ion).unwrap();
967 let ProductDefinitionTemplate::ProbabilityForecast(template) = product.template else {
968 panic!("expected template 4.5");
969 };
970 assert_eq!(
971 template.probability,
972 ProbabilityType::Other {
973 code: 0,
974 lower: None,
975 upper: None,
976 }
977 );
978
979 section[36] = 7;
980 let product = ProductDefinition::parse(§ion).unwrap();
981 let ProductDefinitionTemplate::ProbabilityForecast(template) = product.template else {
982 panic!("expected template 4.5");
983 };
984 assert_eq!(template.probability, ProbabilityType::NearNormal);
985 }
986
987 #[test]
988 fn parses_percentile_forecast_and_rejects_values_above_one_hundred() {
989 let mut section = product_section_template_zero();
990 section.resize(35, 0);
991 set_product_template(&mut section, 6);
992 section[34] = 90;
993
994 let product = ProductDefinition::parse(§ion).unwrap();
995 let ProductDefinitionTemplate::PercentileForecast(template) = product.template else {
996 panic!("expected template 4.6");
997 };
998 assert_eq!(template.percentile_value, 90);
999
1000 section[34] = 101;
1001 assert!(matches!(
1002 ProductDefinition::parse(§ion),
1003 Err(Error::InvalidSection { section: 4, .. })
1004 ));
1005 }
1006
1007 #[test]
1008 fn parses_signed_forecast_time_and_process_metadata() {
1009 let mut section = product_section_template_zero();
1010 section[12] = 7;
1011 section[13] = 42;
1012 section[14..16].copy_from_slice(&12u16.to_be_bytes());
1013 section[16] = 30;
1014 section[18..22].copy_from_slice(&crate::binary::encode_wmo_i32(-6).unwrap());
1015
1016 let product = ProductDefinition::parse(§ion).unwrap();
1017 assert_eq!(product.type_of_generating_process(), Some(2));
1018 assert_eq!(product.generating_process_identifier(), Some(42));
1019 assert_eq!(product.forecast_time(), Some(-6));
1020 let ProductDefinitionTemplate::AnalysisOrForecast(template) = product.template else {
1021 panic!("expected template 4.0");
1022 };
1023 assert_eq!(template.background_generating_process_identifier, 7);
1024 assert_eq!(template.hours_after_data_cutoff, Some(12));
1025 assert_eq!(template.minutes_after_data_cutoff, Some(30));
1026 }
1027
1028 #[test]
1029 fn parses_missing_cutoff_metadata_and_rejects_invalid_minutes() {
1030 let mut section = product_section_template_zero();
1031 section[14..16].copy_from_slice(&u16::MAX.to_be_bytes());
1032 section[16] = u8::MAX;
1033 let product = ProductDefinition::parse(§ion).unwrap();
1034 let ProductDefinitionTemplate::AnalysisOrForecast(template) = product.template else {
1035 panic!("expected template 4.0");
1036 };
1037 assert_eq!(template.hours_after_data_cutoff, None);
1038 assert_eq!(template.minutes_after_data_cutoff, None);
1039
1040 section[16] = 60;
1041 assert!(matches!(
1042 ProductDefinition::parse(§ion),
1043 Err(Error::InvalidSection { section: 4, .. })
1044 ));
1045 }
1046
1047 #[test]
1048 fn parses_statistical_process_template() {
1049 let section = product_section_template_eight();
1050
1051 let product = ProductDefinition::parse(§ion).unwrap();
1052 assert_eq!(product.template_number(), 8);
1053 assert_eq!(product.forecast_time(), Some(6));
1054 assert_eq!(
1055 product.end_of_overall_time_interval(),
1056 Some(ReferenceTime {
1057 year: 2026,
1058 month: 3,
1059 day: 20,
1060 hour: 18,
1061 minute: 0,
1062 second: 0,
1063 })
1064 );
1065 match product.template {
1066 ProductDefinitionTemplate::StatisticalProcess(template) => {
1067 assert_eq!(template.interval.time_ranges.len(), 1);
1068 assert_eq!(
1069 template.interval.time_ranges[0].type_of_statistical_processing,
1070 1
1071 );
1072 assert_eq!(template.interval.time_ranges[0].time_range_length, 6);
1073 }
1074 other => panic!("expected template 4.8, got {other:?}"),
1075 }
1076 }
1077
1078 #[test]
1079 fn parses_ensemble_statistical_process_template() {
1080 let mut section = product_section_template_eight();
1081 section.resize(61, 0);
1082 section[..4].copy_from_slice(&(61u32).to_be_bytes());
1083 section[7..9].copy_from_slice(&11u16.to_be_bytes());
1084 section.copy_within(34..58, 37);
1085 section[34] = 1;
1086 section[35] = 3;
1087 section[36] = 20;
1088
1089 let product = ProductDefinition::parse(§ion).unwrap();
1090 assert_eq!(product.template_number(), 11);
1091 assert_eq!(
1092 product.end_of_overall_time_interval(),
1093 Some(ReferenceTime {
1094 year: 2026,
1095 month: 3,
1096 day: 20,
1097 hour: 18,
1098 minute: 0,
1099 second: 0,
1100 })
1101 );
1102 match product.template {
1103 ProductDefinitionTemplate::EnsembleStatisticalProcess(template) => {
1104 assert_eq!(template.ensemble.perturbation_number, 3);
1105 assert_eq!(template.interval.time_ranges.len(), 1);
1106 }
1107 other => panic!("expected template 4.11, got {other:?}"),
1108 }
1109 }
1110
1111 #[test]
1112 fn parses_probability_statistical_process_template() {
1113 let mut section = product_section_template_zero();
1114 section.resize(71, 0xff);
1115 set_product_template(&mut section, 9);
1116 section[34] = 1;
1117 section[35] = 10;
1118 section[36] = 3;
1119 section[37] = 1;
1120 section[38..42].copy_from_slice(&crate::binary::encode_wmo_i32(125).unwrap());
1121 set_statistical_interval(&mut section, 47);
1122
1123 let product = ProductDefinition::parse(§ion).unwrap();
1124 let ProductDefinitionTemplate::ProbabilityStatisticalProcess(template) = product.template
1125 else {
1126 panic!("expected template 4.9");
1127 };
1128 assert_eq!(
1129 template.probability.probability,
1130 ProbabilityType::AboveLowerLimit(super::ProbabilityLimit {
1131 scale_factor: 1,
1132 scaled_value: 125,
1133 })
1134 );
1135 assert_eq!(template.interval.time_ranges.len(), 1);
1136 assert_eq!(template.interval.end_of_overall_time_interval.hour, 18);
1137 }
1138
1139 #[test]
1140 fn parses_percentile_statistical_process_template() {
1141 let mut section = product_section_template_zero();
1142 section.resize(59, 0);
1143 set_product_template(&mut section, 10);
1144 section[34] = 75;
1145 set_statistical_interval(&mut section, 35);
1146
1147 let product = ProductDefinition::parse(§ion).unwrap();
1148 let ProductDefinitionTemplate::PercentileStatisticalProcess(template) = product.template
1149 else {
1150 panic!("expected template 4.10");
1151 };
1152 assert_eq!(template.percentile.percentile_value, 75);
1153 assert_eq!(template.interval.time_ranges.len(), 1);
1154 }
1155
1156 #[test]
1157 fn parses_derived_statistical_process_template() {
1158 let mut section = product_section_template_zero();
1159 section.resize(60, 0);
1160 set_product_template(&mut section, 12);
1161 section[34] = 1;
1162 section[35] = 20;
1163 set_statistical_interval(&mut section, 36);
1164
1165 let product = ProductDefinition::parse(§ion).unwrap();
1166 let ProductDefinitionTemplate::DerivedStatisticalProcess(template) = product.template
1167 else {
1168 panic!("expected template 4.12");
1169 };
1170 assert_eq!(template.derived.derived_forecast_type, 1);
1171 assert_eq!(template.derived.number_of_forecasts_in_ensemble, 20);
1172 assert_eq!(template.interval.time_ranges.len(), 1);
1173 }
1174
1175 #[test]
1176 fn parses_spatial_process_template() {
1177 let mut section = product_section_template_zero();
1178 section.resize(37, 0);
1179 set_product_template(&mut section, 15);
1180 section[34] = 1;
1181 section[35] = 2;
1182 section[36] = 4;
1183
1184 let product = ProductDefinition::parse(§ion).unwrap();
1185 let ProductDefinitionTemplate::SpatialProcess(template) = product.template else {
1186 panic!("expected template 4.15");
1187 };
1188 assert_eq!(template.statistical_process, 1);
1189 assert_eq!(template.spatial_processing, 2);
1190 assert_eq!(template.number_of_points_used, 4);
1191 assert_eq!(template.base.forecast_time, 6);
1192 }
1193
1194 #[test]
1195 fn rejects_invalid_statistical_process_end_time() {
1196 let mut section = product_section_template_eight();
1197 section[36] = 2;
1198 section[37] = 29;
1199
1200 let err = ProductDefinition::parse(§ion).unwrap_err();
1201 assert!(matches!(err, Error::InvalidSection { section: 4, .. }));
1202 assert!(err.to_string().contains("invalid reference timestamp"));
1203 }
1204
1205 #[test]
1206 fn preserves_unsupported_product_definition_templates() {
1207 let mut section = vec![0u8; 34];
1208 section[..4].copy_from_slice(&(34u32).to_be_bytes());
1209 section[4] = 4;
1210 section[7..9].copy_from_slice(&99u16.to_be_bytes());
1211 section[9] = 2;
1212 section[10] = 3;
1213
1214 section[11..].copy_from_slice(&[0x5a; 23]);
1215
1216 let product = ProductDefinition::parse(§ion).unwrap();
1217 assert_eq!(product.template_number(), 99);
1218 assert_eq!(product.forecast_time(), None);
1219 assert_eq!(product.first_surface(), None);
1220 assert!(matches!(
1221 product.template,
1222 ProductDefinitionTemplate::Unsupported { number: 99, ref raw }
1223 if raw == &[0x5a; 23]
1224 ));
1225 }
1226
1227 #[test]
1228 fn preserves_surface_type_when_numeric_level_is_missing() {
1229 let mut section = product_section_template_zero();
1230 section[23] = 0xff;
1231 section[24..28].copy_from_slice(&[0xff; 4]);
1232
1233 let product = ProductDefinition::parse(§ion).unwrap();
1234 let surface = product.first_surface().unwrap();
1235 assert_eq!(surface.surface_type, 103);
1236 assert_eq!(surface.value, None);
1237 assert_eq!(surface.scaled_value_f64(), None);
1238 }
1239
1240 #[test]
1241 fn rejects_truncated_template_zero_sections() {
1242 let mut section = vec![0u8; 33];
1243 section[..4].copy_from_slice(&(33u32).to_be_bytes());
1244 section[4] = 4;
1245 section[7..9].copy_from_slice(&0u16.to_be_bytes());
1246 section[9] = 2;
1247 section[10] = 3;
1248
1249 let err = ProductDefinition::parse(§ion).unwrap_err();
1250 assert!(matches!(err, Error::InvalidSection { section: 4, .. }));
1251 }
1252
1253 #[test]
1254 fn rejects_truncated_instantaneous_product_templates() {
1255 for (template, length) in [(2, 35), (5, 46), (6, 34), (15, 36)] {
1256 let mut section = product_section_template_zero();
1257 section.resize(length, 0);
1258 set_product_template(&mut section, template);
1259 assert!(matches!(
1260 ProductDefinition::parse(§ion),
1261 Err(Error::InvalidSection { section: 4, .. })
1262 ));
1263 }
1264 }
1265
1266 #[test]
1267 fn rejects_truncated_interval_product_templates() {
1268 for (template, length) in [(8, 45), (9, 58), (10, 46), (11, 48), (12, 47)] {
1269 let mut section = product_section_template_zero();
1270 section.resize(length, 0);
1271 set_product_template(&mut section, template);
1272 assert!(matches!(
1273 ProductDefinition::parse(§ion),
1274 Err(Error::InvalidSection { section: 4, .. })
1275 ));
1276 }
1277 }
1278
1279 fn product_section_template_zero() -> Vec<u8> {
1280 let mut section = vec![0u8; 34];
1281 section[..4].copy_from_slice(&(34u32).to_be_bytes());
1282 section[4] = 4;
1283 section[7..9].copy_from_slice(&0u16.to_be_bytes());
1284 section[9] = 2;
1285 section[10] = 3;
1286 section[11] = 2;
1287 section[17] = 1;
1288 section[18..22].copy_from_slice(&6u32.to_be_bytes());
1289 section[22] = 103;
1290 section[23] = 0;
1291 section[24..28].copy_from_slice(&850u32.to_be_bytes());
1292 section[28] = 255;
1293 section
1294 }
1295
1296 fn product_section_template_eight() -> Vec<u8> {
1297 let mut section = product_section_template_zero();
1298 section.resize(58, 0);
1299 set_product_template(&mut section, 8);
1300 set_statistical_interval(&mut section, 34);
1301 section
1302 }
1303
1304 fn set_product_template(section: &mut [u8], template: u16) {
1305 let length = u32::try_from(section.len()).unwrap();
1306 section[..4].copy_from_slice(&length.to_be_bytes());
1307 section[7..9].copy_from_slice(&template.to_be_bytes());
1308 }
1309
1310 fn set_statistical_interval(section: &mut [u8], offset: usize) {
1311 section[offset..offset + 2].copy_from_slice(&2026u16.to_be_bytes());
1312 section[offset + 2] = 3;
1313 section[offset + 3] = 20;
1314 section[offset + 4] = 18;
1315 section[offset + 5] = 0;
1316 section[offset + 6] = 0;
1317 section[offset + 7] = 1;
1318 section[offset + 8..offset + 12].copy_from_slice(&0u32.to_be_bytes());
1319 let range = offset + 12;
1320 section[range] = 1;
1321 section[range + 1] = 2;
1322 section[range + 2] = 1;
1323 section[range + 3..range + 7].copy_from_slice(&6u32.to_be_bytes());
1324 section[range + 7] = 255;
1325 section[range + 8..range + 12].copy_from_slice(&0u32.to_be_bytes());
1326 }
1327
1328 fn valid_identification_section() -> Vec<u8> {
1329 let mut section = vec![0u8; 21];
1330 section[..4].copy_from_slice(&(21u32).to_be_bytes());
1331 section[4] = 1;
1332 section[5..7].copy_from_slice(&7u16.to_be_bytes());
1333 section[7..9].copy_from_slice(&14u16.to_be_bytes());
1334 section[9] = 35;
1335 section[10] = 1;
1336 section[11] = 1;
1337 section[12..14].copy_from_slice(&2026u16.to_be_bytes());
1338 section[14] = 3;
1339 section[15] = 20;
1340 section[16] = 12;
1341 section[17] = 30;
1342 section[18] = 45;
1343 section[19] = 0;
1344 section[20] = 1;
1345 section
1346 }
1347}