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