Skip to main content

grib/
time.rs

1use std::fmt;
2
3#[cfg(feature = "time-calculation")]
4use chrono::{DateTime, LocalResult, TimeDelta, TimeZone, Utc};
5
6#[cfg(feature = "time-calculation")]
7use crate::codetables::grib2::Table4_4;
8use crate::{Code, ForecastTime, codetables::grib2::Table1_2};
9
10#[derive(Debug, PartialEq, Eq)]
11/// Time-related raw information.
12pub struct TemporalRawInfo {
13    /// "Significance of reference time" set in Section 1 of the submessage. See
14    /// [Code Table 1.2](crate::codetables::grib2::Table1_2).
15    pub ref_time_significance: Code<Table1_2, u8>,
16    /// "Reference time" set in Section 1 of the submessage.
17    pub ref_time_unchecked: crate::def::grib2::RefTime,
18    /// "Forecast time" set in Section 3 of the submessage.
19    pub forecast_time_diff: Option<ForecastTime>,
20}
21
22impl TemporalRawInfo {
23    pub(crate) fn new(
24        ref_time_significance: u8,
25        ref_time_unchecked: crate::def::grib2::RefTime,
26        forecast_time_diff: Option<ForecastTime>,
27    ) -> Self {
28        let ref_time_significance = Table1_2::try_from(ref_time_significance).into();
29        Self {
30            ref_time_significance,
31            ref_time_unchecked,
32            forecast_time_diff,
33        }
34    }
35}
36
37#[cfg(feature = "time-calculation")]
38#[cfg_attr(docsrs, doc(cfg(feature = "time-calculation")))]
39#[derive(Debug, PartialEq, Eq)]
40/// Time-related calculated information.
41pub struct TemporalInfo {
42    /// "Reference time" represented as [`chrono::DateTime`].
43    pub ref_time: Option<DateTime<Utc>>,
44    /// "Forecast time" calculated and represented as [`chrono::DateTime`].
45    pub forecast_time_target: Option<DateTime<Utc>>,
46}
47
48#[cfg(feature = "time-calculation")]
49impl From<&TemporalRawInfo> for TemporalInfo {
50    fn from(value: &TemporalRawInfo) -> Self {
51        let ref_time = create_date_time(
52            value.ref_time_unchecked.year.into(),
53            value.ref_time_unchecked.month.into(),
54            value.ref_time_unchecked.day.into(),
55            value.ref_time_unchecked.hour.into(),
56            value.ref_time_unchecked.minute.into(),
57            value.ref_time_unchecked.second.into(),
58        );
59        let forecast_time_delta = value
60            .forecast_time_diff
61            .as_ref()
62            .and_then(|ft| BasicTimeDelta::new(ft).and_then(|td| td.convert(ft)));
63        let forecast_time_target = ref_time
64            .zip(forecast_time_delta)
65            .map(|(time, delta)| time + delta);
66        Self {
67            ref_time,
68            forecast_time_target,
69        }
70    }
71}
72
73impl crate::def::grib2::RefTime {
74    pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Self {
75        Self {
76            year,
77            month,
78            day,
79            hour,
80            minute,
81            second,
82        }
83    }
84}
85
86impl fmt::Display for crate::def::grib2::RefTime {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        write!(
89            f,
90            "{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
91            self.year, self.month, self.day, self.hour, self.minute, self.second
92        )
93    }
94}
95
96#[cfg(feature = "time-calculation")]
97#[inline]
98fn create_date_time(
99    year: i32,
100    month: u32,
101    day: u32,
102    hour: u32,
103    minute: u32,
104    second: u32,
105) -> Option<DateTime<Utc>> {
106    let result = Utc.with_ymd_and_hms(year, month, day, hour, minute, second);
107    if let LocalResult::None = result {
108        None
109    } else {
110        Some(result.unwrap())
111    }
112}
113
114#[cfg(feature = "time-calculation")]
115struct BasicTimeDelta(fn(i64) -> Option<TimeDelta>, i64);
116
117#[cfg(feature = "time-calculation")]
118impl BasicTimeDelta {
119    fn new(ft: &ForecastTime) -> Option<Self> {
120        match ft.unit {
121            Code::Name(Table4_4::Second) => Some(Self(TimeDelta::try_seconds, 1)),
122            Code::Name(Table4_4::Minute) => Some(Self(TimeDelta::try_minutes, 1)),
123            Code::Name(Table4_4::Hour) => Some(Self(TimeDelta::try_hours, 1)),
124            Code::Name(Table4_4::ThreeHours) => Some(Self(TimeDelta::try_hours, 3)),
125            Code::Name(Table4_4::SixHours) => Some(Self(TimeDelta::try_hours, 6)),
126            Code::Name(Table4_4::TwelveHours) => Some(Self(TimeDelta::try_hours, 12)),
127            Code::Name(Table4_4::Day) => Some(Self(TimeDelta::try_days, 1)),
128            _ => None,
129        }
130    }
131
132    fn convert(&self, ft: &ForecastTime) -> Option<TimeDelta> {
133        let BasicTimeDelta(func, value) = self;
134        func(i64::try_from(ft.value).ok()? * value)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    #[cfg(feature = "time-calculation")]
141    use super::*;
142
143    macro_rules! test_date_time_creation {
144        ($((
145            $name:ident,
146            $year:expr,
147            $month:expr,
148            $day:expr,
149            $hour:expr,
150            $minute:expr,
151            $second:expr,
152            $ok_expected:expr
153        ),)*) => ($(
154            #[cfg(feature = "time-calculation")]
155            #[test]
156            fn $name() {
157                let result = create_date_time($year, $month, $day, $hour, $minute, $second);
158                assert_eq!(result.is_some(), $ok_expected);
159            }
160        )*);
161    }
162
163    test_date_time_creation! {
164        (date_time_creation_for_valid_date_time, 2022, 1, 1, 0, 0, 0, true),
165        (date_time_creation_for_invalid_date, 2022, 11, 31, 0, 0, 0, false),
166        (date_time_creation_for_invalid_time, 2022, 1, 1, 0, 61, 0, false),
167    }
168
169    #[test]
170    fn test_utc_date_time_string() {
171        let time = crate::def::grib2::RefTime::new(2025, 1, 1, 0, 0, 0);
172        assert_eq!(format!("{time}"), "2025-01-01 00:00:00 UTC".to_owned())
173    }
174}