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::{codetables::grib2::Table1_2, Code, ForecastTime};
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: UtcDateTime,
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: UtcDateTime,
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
73#[derive(Debug, PartialEq, Eq)]
74/// UTC date and time container.
75pub struct UtcDateTime {
76    /// Year.
77    pub year: u16,
78    /// Month.
79    pub month: u8,
80    /// Day.
81    pub day: u8,
82    /// Hour.
83    pub hour: u8,
84    /// Minute.
85    pub minute: u8,
86    /// Second.
87    pub second: u8,
88}
89
90impl UtcDateTime {
91    pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Self {
92        Self {
93            year,
94            month,
95            day,
96            hour,
97            minute,
98            second,
99        }
100    }
101}
102
103impl fmt::Display for UtcDateTime {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(
106            f,
107            "{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
108            self.year, self.month, self.day, self.hour, self.minute, self.second
109        )
110    }
111}
112
113#[cfg(feature = "time-calculation")]
114#[inline]
115fn create_date_time(
116    year: i32,
117    month: u32,
118    day: u32,
119    hour: u32,
120    minute: u32,
121    second: u32,
122) -> Option<DateTime<Utc>> {
123    let result = Utc.with_ymd_and_hms(year, month, day, hour, minute, second);
124    if let LocalResult::None = result {
125        None
126    } else {
127        Some(result.unwrap())
128    }
129}
130
131#[cfg(feature = "time-calculation")]
132struct BasicTimeDelta(fn(i64) -> Option<TimeDelta>, i64);
133
134#[cfg(feature = "time-calculation")]
135impl BasicTimeDelta {
136    fn new(ft: &ForecastTime) -> Option<Self> {
137        match ft.unit {
138            Code::Name(Table4_4::Second) => Some(Self(TimeDelta::try_seconds, 1)),
139            Code::Name(Table4_4::Minute) => Some(Self(TimeDelta::try_minutes, 1)),
140            Code::Name(Table4_4::Hour) => Some(Self(TimeDelta::try_hours, 1)),
141            Code::Name(Table4_4::ThreeHours) => Some(Self(TimeDelta::try_hours, 3)),
142            Code::Name(Table4_4::SixHours) => Some(Self(TimeDelta::try_hours, 6)),
143            Code::Name(Table4_4::TwelveHours) => Some(Self(TimeDelta::try_hours, 12)),
144            Code::Name(Table4_4::Day) => Some(Self(TimeDelta::try_days, 1)),
145            _ => None,
146        }
147    }
148
149    fn convert(&self, ft: &ForecastTime) -> Option<TimeDelta> {
150        let BasicTimeDelta(func, value) = self;
151        func(i64::try_from(ft.value).ok()? * value)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    macro_rules! test_date_time_creation {
160        ($((
161            $name:ident,
162            $year:expr,
163            $month:expr,
164            $day:expr,
165            $hour:expr,
166            $minute:expr,
167            $second:expr,
168            $ok_expected:expr
169        ),)*) => ($(
170            #[cfg(feature = "time-calculation")]
171            #[test]
172            fn $name() {
173                let result = create_date_time($year, $month, $day, $hour, $minute, $second);
174                assert_eq!(result.is_some(), $ok_expected);
175            }
176        )*);
177    }
178
179    test_date_time_creation! {
180        (date_time_creation_for_valid_date_time, 2022, 1, 1, 0, 0, 0, true),
181        (date_time_creation_for_invalid_date, 2022, 11, 31, 0, 0, 0, false),
182        (date_time_creation_for_invalid_time, 2022, 1, 1, 0, 61, 0, false),
183    }
184
185    #[test]
186    fn test_utc_date_time_string() {
187        let time = UtcDateTime::new(2025, 1, 1, 0, 0, 0);
188        assert_eq!(format!("{time}"), "2025-01-01 00:00:00 UTC".to_owned())
189    }
190}