shadow_rs/
date_time.rs

1use crate::{Format, SdResult, ShadowError};
2use std::error::Error;
3use time::format_description::well_known::{Rfc2822, Rfc3339};
4#[cfg(feature = "tzdb")]
5use time::UtcOffset;
6use time::{format_description, OffsetDateTime};
7
8pub enum DateTime {
9    Local(OffsetDateTime),
10    Utc(OffsetDateTime),
11}
12
13pub(crate) const DEFINE_SOURCE_DATE_EPOCH: &str = "SOURCE_DATE_EPOCH";
14
15pub fn now_date_time() -> DateTime {
16    // Enable reproducibility for uses of `now_date_time` by respecting the
17    // `SOURCE_DATE_EPOCH` env variable.
18    //
19    // https://reproducible-builds.org/docs/source-date-epoch/
20    match std::env::var_os(DEFINE_SOURCE_DATE_EPOCH) {
21        None => DateTime::now(),
22        Some(timestamp) => {
23            let epoch = timestamp
24                .into_string()
25                .unwrap_or_else(|_| panic!("Input {DEFINE_SOURCE_DATE_EPOCH} could not be parsed"))
26                .parse::<i64>()
27                .unwrap_or_else(|_| {
28                    panic!("Input {DEFINE_SOURCE_DATE_EPOCH} could not be cast to a number")
29                });
30            DateTime::Utc(OffsetDateTime::from_unix_timestamp(epoch).unwrap())
31        }
32    }
33}
34
35impl Default for DateTime {
36    fn default() -> Self {
37        Self::now()
38    }
39}
40
41impl DateTime {
42    pub fn now() -> Self {
43        Self::local_now().unwrap_or_else(|_| DateTime::Utc(OffsetDateTime::now_utc()))
44    }
45
46    pub fn offset_datetime() -> OffsetDateTime {
47        let date_time = Self::now();
48        match date_time {
49            DateTime::Local(time) | DateTime::Utc(time) => time,
50        }
51    }
52
53    #[cfg(not(feature = "tzdb"))]
54    pub fn local_now() -> Result<Self, Box<dyn Error>> {
55        // Warning: This attempts to create a new OffsetDateTime with the current date and time in the local offset, which may fail.
56        // Currently, it always fails on MacOS.
57        // This issue does not exist with the "tzdb" feature (see below), which should be used instead.
58        OffsetDateTime::now_local()
59            .map(DateTime::Local)
60            .map_err(|e| e.into())
61    }
62
63    #[cfg(feature = "tzdb")]
64    pub fn local_now() -> Result<Self, Box<dyn Error>> {
65        let local_time = tzdb::now::local()?;
66        let time_zone_offset =
67            UtcOffset::from_whole_seconds(local_time.local_time_type().ut_offset())?;
68        let local_date_time = OffsetDateTime::from_unix_timestamp(local_time.unix_time())?
69            .to_offset(time_zone_offset);
70        Ok(DateTime::Local(local_date_time))
71    }
72
73    pub fn timestamp_2_utc(time_stamp: i64) -> SdResult<Self> {
74        let time = OffsetDateTime::from_unix_timestamp(time_stamp).map_err(ShadowError::new)?;
75        Ok(DateTime::Utc(time))
76    }
77
78    pub fn from_iso8601_string(iso_string: &str) -> SdResult<Self> {
79        let time = OffsetDateTime::parse(iso_string, &Rfc3339).map_err(ShadowError::new)?;
80        Ok(DateTime::Local(time))
81    }
82
83    pub fn to_rfc2822(&self) -> String {
84        match self {
85            DateTime::Local(dt) | DateTime::Utc(dt) => dt.format(&Rfc2822).unwrap_or_default(),
86        }
87    }
88
89    pub fn to_rfc3339(&self) -> String {
90        match self {
91            DateTime::Local(dt) | DateTime::Utc(dt) => dt.format(&Rfc3339).unwrap_or_default(),
92        }
93    }
94
95    pub fn timestamp(&self) -> i64 {
96        match self {
97            DateTime::Local(dt) | DateTime::Utc(dt) => dt.unix_timestamp(),
98        }
99    }
100}
101
102impl Format for DateTime {
103    fn human_format(&self) -> String {
104        match self {
105            DateTime::Local(dt) | DateTime::Utc(dt) => dt.human_format(),
106        }
107    }
108}
109
110impl Format for OffsetDateTime {
111    fn human_format(&self) -> String {
112        let fmt = format_description::parse(
113            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
114         sign:mandatory]:[offset_minute]",
115        )
116        .unwrap();
117        self.format(&fmt).unwrap()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use human_format_validate::parse_human_format;
125
126    mod human_format_validate {
127        use std::num::{NonZeroU32, NonZeroU8};
128        use winnow::ascii::{digit1, space1};
129        use winnow::error::{ContextError, ParseError};
130        use winnow::token::{literal, take};
131        use winnow::{ModalResult, Parser};
132
133        fn u8_len2(input: &mut &str) -> ModalResult<u8> {
134            take(2_usize).try_map(str::parse).parse_next(input)
135        }
136
137        fn non_zero_u8_len2<const LIMIT: u8>(input: &mut &str) -> ModalResult<NonZeroU8> {
138            take(2_usize)
139                .try_map(str::parse)
140                .verify(|x| *x <= unsafe { NonZeroU8::new_unchecked(LIMIT) })
141                .parse_next(input)
142        }
143
144        //
145        fn non_zero_u32(input: &mut &str) -> ModalResult<NonZeroU32> {
146            digit1.try_map(str::parse).parse_next(input)
147        }
148
149        // 2022-07-14 00:40:05 +08:00
150        pub(crate) fn parse_human_format(
151            input: &str,
152        ) -> Result<(), ParseError<&str, ContextError>> {
153            (
154                non_zero_u32,
155                literal('-'),
156                non_zero_u8_len2::<12>,
157                literal('-'),
158                non_zero_u8_len2::<31>,
159                space1,
160                u8_len2,
161                literal(':'),
162                u8_len2,
163                literal(':'),
164                u8_len2,
165                space1,
166                literal('+'),
167                u8_len2,
168                literal(':'),
169                u8_len2,
170            )
171                .parse(input)?;
172            Ok(())
173        }
174
175        #[test]
176        fn test_parse() {
177            assert!(parse_human_format("2022-07-14 00:40:05 +08:00").is_ok());
178            assert!(parse_human_format("2022-12-14 00:40:05 +08:00").is_ok());
179            assert!(parse_human_format("2022-13-14 00:40:05 +08:00").is_err());
180            assert!(parse_human_format("2022-12-31 00:40:05 +08:00").is_ok());
181            assert!(parse_human_format("2022-12-32 00:40:05 +08:00").is_err());
182            assert!(parse_human_format("2022-07-14 00:40:05 +08:0").is_err());
183            assert!(parse_human_format("2022-07-14 00:40:05 -08:0").is_err());
184            assert!(parse_human_format("2022-07-00 00:40:05 +08:00").is_err());
185            assert!(parse_human_format("2022-00-01 00:40:05 +08:00").is_err());
186            assert!(parse_human_format("2022-00-01 00:40:05 08:00").is_err());
187            assert!(parse_human_format("2022-00-01 00:40:05+08:00").is_err());
188            assert!(parse_human_format("20221-00-01 00:40:05+08:00").is_err());
189            assert!(parse_human_format("20221-01-01 00:40:05 +08:00").is_ok());
190        }
191    }
192
193    #[test]
194    fn test_source_date_epoch() {
195        std::env::set_var(DEFINE_SOURCE_DATE_EPOCH, "1628080443");
196        let time = now_date_time();
197        assert_eq!(time.human_format(), "2021-08-04 12:34:03 +00:00");
198    }
199
200    #[test]
201    fn test_local_now_human_format() {
202        let time = DateTime::local_now().unwrap().human_format();
203        #[cfg(unix)]
204        assert!(!std::fs::read("/etc/localtime").unwrap().is_empty());
205
206        assert!(parse_human_format(&time).is_ok());
207
208        println!("local now:{time}"); // 2022-07-14 00:40:05 +08:00
209        assert_eq!(time.len(), 26);
210    }
211
212    #[test]
213    fn test_timestamp_2_utc() {
214        let time = DateTime::timestamp_2_utc(1628080443).unwrap();
215        assert_eq!(time.to_rfc2822(), "Wed, 04 Aug 2021 12:34:03 +0000");
216        assert_eq!(time.to_rfc3339(), "2021-08-04T12:34:03Z");
217        assert_eq!(time.human_format(), "2021-08-04 12:34:03 +00:00");
218        assert_eq!(time.timestamp(), 1628080443);
219    }
220
221    #[test]
222    fn test_from_iso8601_string() {
223        let time = DateTime::from_iso8601_string("2021-08-04T12:34:03+08:00").unwrap();
224        assert_eq!(time.to_rfc2822(), "Wed, 04 Aug 2021 12:34:03 +0800");
225        assert_eq!(time.to_rfc3339(), "2021-08-04T12:34:03+08:00");
226        assert_eq!(time.human_format(), "2021-08-04 12:34:03 +08:00");
227    }
228}