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)]
11pub struct TemporalRawInfo {
13 pub ref_time_significance: Code<Table1_2, u8>,
16 pub ref_time_unchecked: UtcDateTime,
18 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)]
40pub struct TemporalInfo {
42 pub ref_time: Option<DateTime<Utc>>,
44 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)]
74pub struct UtcDateTime {
76 pub year: u16,
78 pub month: u8,
80 pub day: u8,
82 pub hour: u8,
84 pub minute: u8,
86 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}