Skip to main content

ecbdp_api/
time.rs

1//! Module with time conversion utilities for ECB Data Portal.
2//! 
3//! The utilities defined here convert between the formats used by the ECB and `chrono` library.
4//! The timezones in `chrono` are assumed to be `FixedOffset`.
5
6
7use chrono::{FixedOffset, DateTime, Datelike, TimeZone};
8use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
9use crate::error::Error;
10use crate::parameter::data::PeriodFormat;
11
12
13const PERCENT_ENCODING_FRAGMENT: &AsciiSet = &CONTROLS.add(b':').add(b'+');
14
15
16/// Converts a `DateTime` object into an ECB Data Portal formatted period.
17/// 
18/// # Examples
19/// 
20/// ```rust
21/// use chrono::{DateTime, FixedOffset, TimeZone};
22/// use ecbdp_api::parameter::data::PeriodFormat;
23/// use ecbdp_api::time::datetime_to_ecb_period;
24/// 
25/// const HOUR: i32 = 3600;
26/// 
27/// let datetime: DateTime<FixedOffset> = FixedOffset::east_opt(1 * HOUR).unwrap()
28///     .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
29/// 
30/// assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Annual), String::from("2009"));
31/// assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::SemiAnnual), String::from("2009-S1"));
32/// assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Quarterly), String::from("2009-Q2"));
33/// assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Monthly), String::from("2009-05"));
34/// assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Weekly), String::from("2009-W19"));
35/// assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Daily), String::from("2009-05-15"));
36/// ```
37pub fn datetime_to_ecb_period<Tz>(datetime: &DateTime<Tz>, period: PeriodFormat) -> String
38where
39    Tz: TimeZone,
40    <Tz as TimeZone>::Offset: std::fmt::Display,
41{
42    let chrono_format: String = match period {
43        PeriodFormat::Annual => "%Y".to_owned(),
44        PeriodFormat::SemiAnnual => {
45            let half_year: usize = if datetime.month() <=6 { 1 } else { 2 };
46            format!("%Y-S{half_year}")
47        },
48        PeriodFormat::Quarterly => "%Y-Q%q".to_owned(),
49        PeriodFormat::Monthly => "%Y-%m".to_owned(),
50        PeriodFormat::Weekly => "%Y-W%U".to_owned(),
51        PeriodFormat::Daily => "%Y-%m-%d".to_owned(),
52    };
53    format!("{}", datetime.format(&chrono_format))
54}
55
56
57/// Converts an ECB Data Portal formatted datetime into a `DateTime` object.
58/// 
59/// # Examples
60/// 
61/// ```rust
62/// use chrono::{DateTime, Datelike, FixedOffset, Timelike};
63/// use ecbdp_api::time::ecb_string_to_datetime;
64/// 
65/// const HOUR: i32 = 3600;
66/// 
67/// let datetime_str: String = String::from("2025-03-12T23:59:59.999+01:00");
68/// let datetime: DateTime<FixedOffset> = ecb_string_to_datetime(&datetime_str).unwrap();
69/// 
70/// assert_eq!(datetime.year(), 2025);
71/// assert_eq!(datetime.month(), 3);
72/// assert_eq!(datetime.day(), 12);
73/// assert_eq!(datetime.hour(), 23);
74/// assert_eq!(datetime.minute(), 59);
75/// assert_eq!(datetime.second(), 59);
76/// assert_eq!(datetime.timezone(), FixedOffset::east_opt(1 * HOUR).unwrap())
77/// ```
78pub fn ecb_string_to_datetime(ecb_datetime: &str) -> Result<DateTime<FixedOffset>, Error> {
79    Ok(DateTime::parse_from_rfc3339(ecb_datetime)?)
80}
81
82
83/// Percent econdes datetime string to be included in a URL.
84/// 
85/// # Examples
86/// 
87/// ```rust
88/// use chrono::{DateTime, FixedOffset, TimeZone};
89/// use ecbdp_api::time::percent_encode_datetime;
90/// 
91/// const HOUR: i32 = 3600;
92/// 
93/// // East offset
94/// let datetime: DateTime<FixedOffset> = FixedOffset::east_opt(1 * HOUR).unwrap()
95///     .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
96/// let encoded_datetime: String = String::from("2009-05-15T14%3A15%3A00%2B01%3A00");
97/// assert_eq!(percent_encode_datetime(&datetime), encoded_datetime);
98/// 
99/// // West offset
100/// let datetime: DateTime<FixedOffset> = FixedOffset::west_opt(1 * HOUR).unwrap()
101///     .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
102/// let encoded_datetime: String = String::from("2009-05-15T14%3A15%3A00-01%3A00");
103/// assert_eq!(percent_encode_datetime(&datetime), encoded_datetime);
104/// ```
105pub fn percent_encode_datetime<Tz: TimeZone>(datetime: &DateTime<Tz>) -> String {
106    let datetime_str: String = datetime.to_rfc3339();
107    utf8_percent_encode(&datetime_str, PERCENT_ENCODING_FRAGMENT).to_string()
108}
109
110
111#[cfg(test)]
112mod tests {
113    use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike};
114
115    /// Number of seconds in an hour.
116    const HOUR: i32 = 3600;
117
118    #[test]
119    fn unit_test_datetime_to_ecb_period() -> () {
120        use crate::parameter::data::PeriodFormat;
121        use crate::time::datetime_to_ecb_period;
122        let datetime: DateTime<FixedOffset> = FixedOffset::east_opt(1 * HOUR).unwrap()
123            .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
124        assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Annual), String::from("2009"));
125        assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::SemiAnnual), String::from("2009-S1"));
126        assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Quarterly), String::from("2009-Q2"));
127        assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Monthly), String::from("2009-05"));
128        assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Weekly), String::from("2009-W19"));
129        assert_eq!(datetime_to_ecb_period(&datetime, PeriodFormat::Daily), String::from("2009-05-15"));
130    }
131
132    #[test]
133    fn unit_test_ecb_string_to_datetime() -> () {
134        use crate::time::ecb_string_to_datetime;
135        let datetime_str: String = String::from("2025-03-12T23:59:59.999+01:00");
136        let datetime: DateTime<FixedOffset> = ecb_string_to_datetime(&datetime_str).unwrap();
137        assert_eq!(datetime.year(), 2025);
138        assert_eq!(datetime.month(), 3);
139        assert_eq!(datetime.day(), 12);
140        assert_eq!(datetime.hour(), 23);
141        assert_eq!(datetime.minute(), 59);
142        assert_eq!(datetime.second(), 59);
143        assert_eq!(datetime.timezone(), FixedOffset::east_opt(1 * HOUR).unwrap())
144    }
145
146    #[test]
147    fn unit_test_percent_encode_datetime() -> () {
148        use crate::time::percent_encode_datetime;
149        // East offset
150        let datetime: DateTime<FixedOffset> = FixedOffset::east_opt(1 * HOUR).unwrap()
151            .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
152        let encoded_datetime: String = String::from("2009-05-15T14%3A15%3A00%2B01%3A00");
153        assert_eq!(percent_encode_datetime(&datetime), encoded_datetime);
154        // West offset
155        let datetime: DateTime<FixedOffset> = FixedOffset::west_opt(1 * HOUR).unwrap()
156            .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
157        let encoded_datetime: String = String::from("2009-05-15T14%3A15%3A00-01%3A00");
158        assert_eq!(percent_encode_datetime(&datetime), encoded_datetime);
159    }
160}