portable_network_archive/cli/value/
datetime.rs1use pna::Duration;
2use std::{
3 borrow::Cow,
4 fmt::{self, Display, Formatter},
5 str::FromStr,
6 time::{SystemTime, UNIX_EPOCH},
7};
8
9#[derive(Debug, thiserror::Error)]
10pub enum DateTimeError {
11 #[error("Failed to parse seconds since unix epoch")]
12 InvalidNumber,
13 #[error("Failed to parse seconds since unix epoch")]
14 ParseInt(#[from] std::num::ParseIntError),
15 #[error(transparent)]
16 ChronoParse(#[from] chrono::ParseError),
17 #[error(transparent)]
18 ParseDateTime(#[from] parse_datetime::ParseDateTimeError),
19}
20
21#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
22pub enum DateTime {
23 Naive(chrono::NaiveDateTime),
24 Zoned(jiff::Zoned),
25 Date(chrono::NaiveDate),
26 Epoch(i64, u32), }
28
29impl DateTime {
30 #[inline]
31 pub fn to_system_time(&self) -> SystemTime {
32 #[inline]
33 fn from_timestamp(seconds: i64, nanoseconds: u32) -> SystemTime {
34 UNIX_EPOCH + Duration::new(seconds, nanoseconds as _)
35 }
36 match self {
37 Self::Naive(naive) => {
38 let (seconds, nanos) = match naive.and_local_timezone(chrono::Local) {
39 chrono::LocalResult::Single(local) => {
40 (local.timestamp(), local.timestamp_subsec_nanos())
41 }
42 chrono::LocalResult::Ambiguous(earlier, _) => {
43 (earlier.timestamp(), earlier.timestamp_subsec_nanos())
44 }
45 chrono::LocalResult::None => {
46 let utc = naive.and_utc();
48 (utc.timestamp(), utc.timestamp_subsec_nanos())
49 }
50 };
51 from_timestamp(seconds, nanos)
52 }
53 Self::Zoned(zoned) => {
54 let ts = zoned.timestamp();
55 from_timestamp(ts.as_second(), zoned.subsec_nanosecond() as u32)
56 }
57 Self::Date(date) => {
58 let utc = date.and_hms_opt(0, 0, 0).unwrap().and_utc();
59 from_timestamp(utc.timestamp(), utc.timestamp_subsec_nanos())
60 }
61 Self::Epoch(seconds, nanos) => from_timestamp(*seconds, *nanos),
62 }
63 }
64}
65
66impl Display for DateTime {
67 #[inline]
68 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::Naive(naive) => Display::fmt(naive, f),
71 Self::Zoned(zoned) => Display::fmt(zoned, f),
72 Self::Date(date) => Display::fmt(date, f),
73 Self::Epoch(seconds, nanos) => write!(f, "@{seconds}.{nanos:09}"),
74 }
75 }
76}
77
78impl FromStr for DateTime {
79 type Err = DateTimeError;
80
81 #[inline]
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 if let Some(seconds) = s.strip_prefix('@') {
84 let seconds_str = if seconds.contains(',') {
86 Cow::Owned(seconds.replace(',', "."))
87 } else {
88 Cow::Borrowed(seconds)
89 };
90 let mut split = seconds_str.splitn(2, '.');
92 let int_part = split.next().expect("split always has at least one part");
93 let frac_part = split.next();
94
95 let secs = i64::from_str(int_part)?;
97
98 let nanos: u32 = if let Some(frac) = frac_part {
100 if !frac.bytes().all(|c| c.is_ascii_digit()) {
102 return Err(Self::Err::InvalidNumber);
103 }
104 let digits = frac.as_bytes();
106 let mut ns: u32 = 0;
107 for &b in digits.iter().chain(std::iter::repeat(&b'0')).take(9) {
109 ns = (ns * 10) + (b - b'0') as u32;
110 }
111 ns
112 } else {
113 0
114 };
115 Ok(Self::Epoch(secs, nanos))
116 } else if let Ok(naive) = chrono::NaiveDateTime::from_str(s) {
117 Ok(Self::Naive(naive))
118 } else if let Ok(naive_date) = chrono::NaiveDate::from_str(s) {
119 Ok(Self::Date(naive_date))
120 } else {
121 Ok(Self::Zoned(parse_datetime::parse_datetime(s)?))
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_datetime_parse_valid() {
132 let valid_dt = "2024-03-20T12:34:56";
133 let datetime = DateTime::from_str(valid_dt).unwrap();
134 assert_eq!(datetime.to_string(), "2024-03-20 12:34:56");
135 }
136
137 #[test]
138 fn test_datetime_parse_with_timezone() {
139 let zoned_dt = "2024-03-20T12:34:56+09:00";
140 let datetime = DateTime::from_str(zoned_dt).unwrap();
141 assert_eq!(datetime.to_string(), "2024-03-20T12:34:56+09:00[+09:00]");
142 let zoned_dt = "2024-03-20T12:34:56Z";
143 let datetime = DateTime::from_str(zoned_dt).unwrap();
144 assert_eq!(datetime.to_string(), "2024-03-20T12:34:56+00:00[UTC]");
145 }
146
147 #[test]
148 fn test_datetime_parse_invalid() {
149 let invalid_dt = "invalid-datetime";
150 assert!(DateTime::from_str(invalid_dt).is_err());
151 }
152
153 #[test]
154 fn test_to_system_time_after_epoch() {
155 let positive_dt = "2024-03-20T12:34:56Z";
156 let datetime = DateTime::from_str(positive_dt).unwrap();
157 let system_time = datetime.to_system_time();
158 assert!(system_time > UNIX_EPOCH);
159 }
160
161 #[cfg(not(target_family = "wasm"))]
162 #[test]
163 fn test_to_system_time_before_epoch() {
164 let negative_dt = "1969-12-31T23:59:59Z";
165 let datetime = DateTime::from_str(negative_dt).unwrap();
166 let system_time = datetime.to_system_time();
167 assert!(system_time < UNIX_EPOCH);
168 }
169
170 #[test]
171 fn test_relative_time_format_positive() {
172 let datetime = DateTime::from_str("@1234567890").unwrap();
173 assert_eq!(datetime.to_string(), "@1234567890.000000000");
174 }
175
176 #[test]
177 fn test_relative_time_format_negative() {
178 let datetime = DateTime::from_str("@-1234567890").unwrap();
179 assert_eq!(datetime.to_string(), "@-1234567890.000000000");
180 }
181
182 #[test]
183 fn test_relative_time_format_tailing_decimal_dot() {
184 let datetime = DateTime::from_str("@123.").unwrap();
185 assert_eq!(datetime.to_string(), "@123.000000000");
186 }
187
188 #[test]
189 fn test_relative_time_format_decimal_dot_zeros() {
190 let datetime = DateTime::from_str("@123.0").unwrap();
191 assert_eq!(datetime.to_string(), "@123.000000000");
192 }
193
194 #[test]
195 fn test_relative_time_format_decimal_dot_zero_one() {
196 let datetime = DateTime::from_str("@123.01").unwrap();
197 assert_eq!(datetime.to_string(), "@123.010000000");
198 }
199
200 #[test]
201 fn test_relative_time_format_decimal_dot() {
202 let datetime = DateTime::from_str("@123.456").unwrap();
203 assert_eq!(datetime.to_string(), "@123.456000000");
204 }
205
206 #[test]
207 fn test_relative_time_format_decimal_comma() {
208 let datetime = DateTime::from_str("@123,456").unwrap();
209 assert_eq!(datetime.to_string(), "@123.456000000");
210 }
211
212 #[test]
213 fn test_relative_time_format_negative_decimal_dot() {
214 let datetime = DateTime::from_str("@-123.456").unwrap();
215 assert_eq!(datetime.to_string(), "@-123.456000000");
216 }
217
218 #[test]
219 fn test_relative_time_format_negative_decimal_comma() {
220 let datetime = DateTime::from_str("@-123,456").unwrap();
221 assert_eq!(datetime.to_string(), "@-123.456000000");
222 }
223
224 #[test]
225 fn test_relative_time_format_zero() {
226 let datetime = DateTime::from_str("@0").unwrap();
227 assert_eq!(datetime.to_string(), "@0.000000000");
228 }
229
230 #[test]
231 fn test_relative_time_format_negative_one() {
232 let datetime = DateTime::from_str("@-1").unwrap();
233 assert_eq!(datetime.to_string(), "@-1.000000000");
234 }
235
236 #[test]
237 fn test_datetime_parse_and_display_date() {
238 let datetime = DateTime::from_str("2024-04-01").unwrap();
239 assert_eq!(datetime.to_string(), "2024-04-01");
240 }
241
242 #[test]
243 fn test_to_system_time_naive() {
244 let naive = chrono::NaiveDate::from_ymd_opt(2024, 4, 1)
245 .unwrap()
246 .and_hms_opt(12, 0, 0)
247 .unwrap();
248 let datetime = DateTime::Naive(naive);
249 let system_time = datetime.to_system_time();
250 assert!(system_time > UNIX_EPOCH);
251 }
252
253 #[test]
254 fn test_to_system_time_date() {
255 let date = chrono::NaiveDate::from_ymd_opt(2024, 4, 1).unwrap();
256 let datetime = DateTime::Date(date);
257 let system_time = datetime.to_system_time();
258 assert!(system_time > UNIX_EPOCH);
259 }
260
261 #[test]
262 fn test_to_system_time_epoch() {
263 let datetime = DateTime::Epoch(1234567890, 0);
264 let system_time = datetime.to_system_time();
265 assert!(system_time > UNIX_EPOCH);
266 }
267}