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 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 fn non_zero_u32(input: &mut &str) -> ModalResult<NonZeroU32> {
129 digit1.try_map(str::parse).parse_next(input)
130 }
131
132 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}