Skip to main content

shadow_rs/
date_time.rs

1use crate::{Format, SdResult, ShadowError};
2
3pub struct DateTime(jiff::Zoned);
4
5pub(crate) const DEFINE_SOURCE_DATE_EPOCH: &str = "SOURCE_DATE_EPOCH";
6
7pub fn now_date_time() -> DateTime {
8    // Enable reproducibility for uses of `now_date_time` by respecting the
9    // `SOURCE_DATE_EPOCH` env variable.
10    //
11    // https://reproducible-builds.org/docs/source-date-epoch/
12    match std::env::var_os(DEFINE_SOURCE_DATE_EPOCH) {
13        None => DateTime::now(),
14        Some(timestamp) => {
15            let epoch = timestamp
16                .into_string()
17                .unwrap_or_else(|_| panic!("Input {DEFINE_SOURCE_DATE_EPOCH} could not be parsed"))
18                .parse::<i64>()
19                .unwrap_or_else(|_| {
20                    panic!("Input {DEFINE_SOURCE_DATE_EPOCH} could not be cast to a number")
21                });
22            DateTime(
23                jiff::Timestamp::from_second(epoch)
24                    .unwrap()
25                    .to_zoned(jiff::tz::TimeZone::UTC),
26            )
27        }
28    }
29}
30
31impl Default for DateTime {
32    fn default() -> Self {
33        Self::now()
34    }
35}
36
37impl DateTime {
38    pub fn new(zoned: jiff::Zoned) -> Self {
39        Self(zoned)
40    }
41
42    pub fn now() -> Self {
43        Self(jiff::Zoned::now())
44    }
45
46    pub fn timestamp_2_utc(time_stamp: i64) -> SdResult<Self> {
47        let utc_time = jiff::Timestamp::from_second(time_stamp).map_err(ShadowError::new)?;
48        let zoned = utc_time.to_zoned(jiff::tz::TimeZone::UTC);
49        Ok(DateTime::new(zoned))
50    }
51
52    pub fn from_iso8601_string(iso_string: &str) -> SdResult<Self> {
53        let pieces = jiff::fmt::temporal::Pieces::parse(iso_string).map_err(ShadowError::new)?;
54
55        let time = match pieces.time() {
56            Some(time) => time,
57            None => {
58                return Err(ShadowError::from(
59                    "iso string has no time, and thus cannot be parsed into a datetime".to_string(),
60                ));
61            }
62        };
63        let dt = pieces.date().to_datetime(time);
64        let offset = match pieces.to_numeric_offset() {
65            Some(offset) => offset,
66            None => {
67                return Err(ShadowError::from(
68                    "iso string has no offset, and thus cannot be parsed into a datetime"
69                        .to_string(),
70                ));
71            }
72        };
73        let zoned = jiff::tz::TimeZone::fixed(offset)
74            .to_zoned(dt)
75            .map_err(ShadowError::new)?;
76
77        Ok(DateTime::new(zoned))
78    }
79
80    pub fn to_rfc2822(&self) -> String {
81        jiff::fmt::rfc2822::to_string(&self.0).unwrap_or_default()
82    }
83
84    pub fn to_rfc3339(&self) -> String {
85        let ts = self.0.timestamp();
86        let offset = self.0.offset();
87        if self.0.time_zone() == &jiff::tz::TimeZone::UTC {
88            ts.to_string()
89        } else {
90            ts.display_with_offset(offset).to_string()
91        }
92    }
93
94    pub fn timestamp(&self) -> i64 {
95        self.0.timestamp().as_second()
96    }
97}
98
99impl Format for DateTime {
100    fn human_format(&self) -> String {
101        self.0.strftime("%Y-%m-%d %H:%M:%S %:z").to_string()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    mod human_format_validate {
110        use std::num::{NonZeroU32, NonZeroU8};
111        use winnow::ascii::{digit1, space1};
112        use winnow::error::{ContextError, ParseError};
113        use winnow::token::{literal, take};
114        use winnow::{ModalResult, Parser};
115
116        fn u8_len2(input: &mut &str) -> ModalResult<u8> {
117            take(2_usize).try_map(str::parse).parse_next(input)
118        }
119
120        fn non_zero_u8_len2<const LIMIT: u8>(input: &mut &str) -> ModalResult<NonZeroU8> {
121            take(2_usize)
122                .try_map(str::parse)
123                .verify(|x| *x <= unsafe { NonZeroU8::new_unchecked(LIMIT) })
124                .parse_next(input)
125        }
126
127        //
128        fn non_zero_u32(input: &mut &str) -> ModalResult<NonZeroU32> {
129            digit1.try_map(str::parse).parse_next(input)
130        }
131
132        // 2022-07-14 00:40:05 +08:00
133        pub(crate) fn parse_human_format(
134            input: &str,
135        ) -> Result<(), ParseError<&str, ContextError>> {
136            (
137                non_zero_u32,
138                literal('-'),
139                non_zero_u8_len2::<12>,
140                literal('-'),
141                non_zero_u8_len2::<31>,
142                space1,
143                u8_len2,
144                literal(':'),
145                u8_len2,
146                literal(':'),
147                u8_len2,
148                space1,
149                literal('+'),
150                u8_len2,
151                literal(':'),
152                u8_len2,
153            )
154                .parse(input)?;
155            Ok(())
156        }
157
158        #[test]
159        fn test_parse() {
160            assert!(parse_human_format("2022-07-14 00:40:05 +08:00").is_ok());
161            assert!(parse_human_format("2022-12-14 00:40:05 +08:00").is_ok());
162            assert!(parse_human_format("2022-13-14 00:40:05 +08:00").is_err());
163            assert!(parse_human_format("2022-12-31 00:40:05 +08:00").is_ok());
164            assert!(parse_human_format("2022-12-32 00:40:05 +08:00").is_err());
165            assert!(parse_human_format("2022-07-14 00:40:05 +08:0").is_err());
166            assert!(parse_human_format("2022-07-14 00:40:05 -08:0").is_err());
167            assert!(parse_human_format("2022-07-00 00:40:05 +08:00").is_err());
168            assert!(parse_human_format("2022-00-01 00:40:05 +08:00").is_err());
169            assert!(parse_human_format("2022-00-01 00:40:05 08:00").is_err());
170            assert!(parse_human_format("2022-00-01 00:40:05+08:00").is_err());
171            assert!(parse_human_format("20221-00-01 00:40:05+08:00").is_err());
172            assert!(parse_human_format("20221-01-01 00:40:05 +08:00").is_ok());
173        }
174    }
175
176    #[test]
177    fn test_source_date_epoch() {
178        std::env::set_var(DEFINE_SOURCE_DATE_EPOCH, "1628080443");
179        let time = now_date_time();
180        assert_eq!(time.human_format(), "2021-08-04 12:34:03 +00:00");
181    }
182
183    #[test]
184    fn test_timestamp_2_utc() {
185        let time = DateTime::timestamp_2_utc(1628080443).unwrap();
186        assert_eq!(time.to_rfc2822(), "Wed, 4 Aug 2021 12:34:03 +0000");
187        assert_eq!(time.to_rfc3339(), "2021-08-04T12:34:03Z");
188        assert_eq!(time.human_format(), "2021-08-04 12:34:03 +00:00");
189        assert_eq!(time.timestamp(), 1628080443);
190    }
191
192    #[test]
193    fn test_from_iso8601_string() {
194        let time = DateTime::from_iso8601_string("2021-08-04T12:34:03+08:00").unwrap();
195        assert_eq!(time.to_rfc2822(), "Wed, 4 Aug 2021 12:34:03 +0800");
196        assert_eq!(time.to_rfc3339(), "2021-08-04T12:34:03+08:00");
197        assert_eq!(time.human_format(), "2021-08-04 12:34:03 +08:00");
198    }
199}