azure_core/date/
mod.rs

1//! Azure date and time parsing and formatting
2
3// RFC 3339 vs ISO 8601
4// <https://ijmacd.github.io/rfc3339-iso8601/>
5
6use crate::error::{ErrorKind, ResultExt};
7use std::time::Duration;
8use time::{
9    format_description::{well_known::Rfc3339, FormatItem},
10    macros::format_description,
11    OffsetDateTime, PrimitiveDateTime, UtcOffset,
12};
13
14// Serde modules
15pub use time::serde::rfc3339;
16pub use time::serde::timestamp;
17pub mod iso8601;
18pub mod rfc1123;
19
20/// RFC 3339: Date and Time on the Internet: Timestamps
21///
22/// <https://www.rfc-editor.org/rfc/rfc3339>
23///
24/// In Azure REST API specifications it is specified as `"format": "date-time"`.
25///
26/// 1985-04-12T23:20:50.52Z
27pub fn parse_rfc3339(s: &str) -> crate::Result<OffsetDateTime> {
28    OffsetDateTime::parse(s, &Rfc3339).with_context(ErrorKind::DataConversion, || {
29        format!("unable to parse rfc3339 date '{s}")
30    })
31}
32
33/// RFC 3339: Date and Time on the Internet: Timestamps
34///
35/// <https://www.rfc-editor.org/rfc/rfc3339>
36///
37/// In Azure REST API specifications it is specified as `"format": "date-time"`.
38///
39/// 1985-04-12T23:20:50.52Z
40pub fn to_rfc3339(date: &OffsetDateTime) -> String {
41    // known format does not panic
42    date.format(&Rfc3339).unwrap()
43}
44
45/// RFC 1123: Requirements for Internet Hosts - Application and Support
46///
47/// <https://www.rfc-editor.org/rfc/rfc1123>
48///
49/// In Azure REST API specifications it is specified as `"format": "date-time-rfc1123"`.
50///
51/// In .NET it is the `rfc1123pattern`.
52/// <https://docs.microsoft.com/dotnet/api/system.globalization.datetimeformatinfo.rfc1123pattern>
53///
54/// This format is also the preferred HTTP date format.
55/// <https://httpwg.org/specs/rfc9110.html#http.date>
56///
57/// Sun, 06 Nov 1994 08:49:37 GMT
58pub fn parse_rfc1123(s: &str) -> crate::Result<OffsetDateTime> {
59    Ok(PrimitiveDateTime::parse(s, RFC1123_FORMAT)
60        .with_context(ErrorKind::DataConversion, || {
61            format!("unable to parse rfc1123 date '{s}")
62        })?
63        .assume_utc())
64}
65
66const RFC1123_FORMAT: &[FormatItem] = format_description!(
67    "[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT"
68);
69
70/// RFC 1123: Requirements for Internet Hosts - Application and Support
71///
72/// <https://www.rfc-editor.org/rfc/rfc1123>
73///
74/// In Azure REST API specifications it is specified as `"format": "date-time-rfc1123"`.
75///
76/// In .NET it is the `rfc1123pattern`.
77/// <https://docs.microsoft.com/dotnet/api/system.globalization.datetimeformatinfo.rfc1123pattern>
78///
79/// This format is also the preferred HTTP date format.
80/// <https://httpwg.org/specs/rfc9110.html#http.date>
81///
82/// Sun, 06 Nov 1994 08:49:37 GMT
83pub fn to_rfc1123(date: &OffsetDateTime) -> String {
84    date.to_offset(UtcOffset::UTC);
85    // known format does not panic
86    date.format(&RFC1123_FORMAT).unwrap()
87}
88
89/// Similar to RFC 1123, but includes milliseconds.
90///
91/// <https://docs.microsoft.com/rest/api/cosmos-db/patch-a-document>
92///
93/// x-ms-last-state-change-utc: Fri, 25 Mar 2016 21:27:20.035 GMT
94pub fn parse_last_state_change(s: &str) -> crate::Result<OffsetDateTime> {
95    Ok(PrimitiveDateTime::parse(s, LAST_STATE_CHANGE_FORMAT)
96        .with_context(ErrorKind::DataConversion, || {
97            format!("unable to parse last state change date '{s}")
98        })?
99        .assume_utc())
100}
101
102const LAST_STATE_CHANGE_FORMAT: &[FormatItem] = format_description!(
103    "[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second].[subsecond digits:3] GMT"
104);
105
106/// Similar to preferred HTTP date format, but includes milliseconds
107///
108/// <https://docs.microsoft.com/rest/api/cosmos-db/patch-a-document>
109///
110/// x-ms-last-state-change-utc: Fri, 25 Mar 2016 21:27:20.035 GMT
111pub fn to_last_state_change(date: &OffsetDateTime) -> String {
112    date.to_offset(UtcOffset::UTC);
113    // known format does not panic
114    date.format(LAST_STATE_CHANGE_FORMAT).unwrap()
115}
116
117// Create a duration from the number of minutes.
118pub fn duration_from_minutes(minutes: u64) -> Duration {
119    Duration::from_secs(minutes * 60)
120}
121
122// Create a duration from the number of hours.
123pub fn duration_from_hours(hours: u64) -> Duration {
124    Duration::from_secs(hours * 3_600)
125}
126
127// Create a duration from the number of days.
128pub fn duration_from_days(days: u64) -> Duration {
129    Duration::from_secs(days * 86_400)
130}
131
132/// Get the difference between two dates.
133pub fn diff(first: OffsetDateTime, second: OffsetDateTime) -> Duration {
134    (first - second).unsigned_abs()
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::from_json;
141    use serde::{Deserialize, Serialize};
142    use time::macros::datetime;
143
144    #[derive(Serialize, Deserialize)]
145    struct ExampleState {
146        #[serde(with = "crate::date::rfc3339")]
147        created_time: time::OffsetDateTime,
148
149        // Note: Must specify "default" in serde options when using "with"
150        #[serde(default, with = "crate::date::rfc3339::option")]
151        deleted_time: Option<time::OffsetDateTime>,
152    }
153
154    #[test]
155    fn test_roundtrip_rfc3339() -> crate::Result<()> {
156        let s = "2019-10-12T07:20:50.52Z";
157        let dt = parse_rfc3339(s)?;
158        assert_eq!(s, to_rfc3339(&dt));
159        Ok(())
160    }
161
162    #[test]
163    fn test_device_update_dates() -> crate::Result<()> {
164        let created = parse_rfc3339("1999-09-10T21:59:22Z")?;
165        let last_action = parse_rfc3339("1999-09-10T03:05:07.3845533+01:00")?;
166        assert_eq!(created, datetime!(1999-09-10 21:59:22 UTC));
167        assert_eq!(last_action, datetime!(1999-09-10 03:05:07.3845533 +01));
168        Ok(())
169    }
170
171    #[test]
172    fn test_to_rfc1123() -> crate::Result<()> {
173        let dt = datetime!(1994-11-06 08:49:37 UTC);
174        assert_eq!("Sun, 06 Nov 1994 08:49:37 GMT", to_rfc1123(&dt));
175        Ok(())
176    }
177
178    #[test]
179    fn test_parse_rfc1123() -> crate::Result<()> {
180        let dt = datetime!(1994-11-06 08:49:37 UTC);
181        assert_eq!(parse_rfc1123("Sun, 06 Nov 1994 08:49:37 GMT")?, dt);
182        Ok(())
183    }
184
185    #[test]
186    fn test_parse_last_state_change() -> crate::Result<()> {
187        assert_eq!(
188            datetime!(2020-01-15 23:39:44.369 UTC),
189            parse_last_state_change("Wed, 15 Jan 2020 23:39:44.369 GMT")?
190        );
191        Ok(())
192    }
193
194    #[test]
195    fn test_list_blob_creation_time() -> crate::Result<()> {
196        let creation_time = "Thu, 01 Jul 2021 10:45:02 GMT";
197        assert_eq!(
198            datetime!(2021-07-01 10:45:02 UTC),
199            parse_rfc1123(creation_time)?
200        );
201        Ok(())
202    }
203
204    #[test]
205    fn test_serde_rfc3339_none_optional() -> crate::Result<()> {
206        let json_state = r#"{
207            "created_time": "2021-07-01T10:45:02Z"
208        }"#;
209
210        let state: ExampleState = from_json(json_state)?;
211
212        assert_eq!(parse_rfc3339("2021-07-01T10:45:02Z")?, state.created_time);
213        assert_eq!(state.deleted_time, None);
214
215        Ok(())
216    }
217
218    #[test]
219    fn test_serde_rfc3339_some_optional() -> crate::Result<()> {
220        let json_state = r#"{
221            "created_time": "2021-07-01T10:45:02Z",
222            "deleted_time": "2022-03-28T11:05:31Z"
223        }"#;
224
225        let state: ExampleState = from_json(json_state)?;
226
227        assert_eq!(parse_rfc3339("2021-07-01T10:45:02Z")?, state.created_time);
228        assert_eq!(
229            state.deleted_time,
230            Some(parse_rfc3339("2022-03-28T11:05:31Z")?)
231        );
232
233        Ok(())
234    }
235}