1use std::fmt::Write;
7
8use chrono::{DateTime, Duration, Local, TimeZone, Utc};
9
10use crate::error::message::MessageError;
11
12const SEPARATOR: &str = ", ";
13
14pub const TIMESTAMP_FACTOR: i64 = 1_000_000_000;
19
20#[must_use]
33pub fn get_offset() -> i64 {
34 Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0)
35 .unwrap()
36 .timestamp()
37}
38
39pub fn get_local_time(date_stamp: &i64, offset: &i64) -> Result<DateTime<Local>, MessageError> {
53 let seconds_since_2001 = if *date_stamp >= 1_000_000_000_000 {
56 date_stamp / TIMESTAMP_FACTOR
57 } else {
58 *date_stamp
59 };
60
61 let utc_stamp = DateTime::from_timestamp(seconds_since_2001 + offset, 0)
62 .ok_or(MessageError::InvalidTimestamp(*date_stamp))?
63 .naive_utc();
64 Ok(Local.from_utc_datetime(&utc_stamp))
65}
66
67#[must_use]
79pub fn format(date: &Result<DateTime<Local>, MessageError>) -> String {
80 match date {
81 Ok(d) => DateTime::format(d, "%b %d, %Y %l:%M:%S %p").to_string(),
82 Err(why) => why.to_string(),
83 }
84}
85
86#[must_use]
99pub fn readable_diff(
100 start: Result<DateTime<Local>, MessageError>,
101 end: Result<DateTime<Local>, MessageError>,
102) -> Option<String> {
103 let diff: Duration = end.ok()? - start.ok()?;
105 let seconds = diff.num_seconds();
106
107 if seconds < 0 {
109 return None;
110 }
111
112 let mut out_s = String::with_capacity(42);
116
117 let days = seconds / 86400;
118 let hours = (seconds % 86400) / 3600;
119 let minutes = (seconds % 86400 % 3600) / 60;
120 let secs = seconds % 86400 % 3600 % 60;
121
122 if days != 0 {
123 let metric = match days {
124 1 => "day",
125 _ => "days",
126 };
127 let _ = write!(out_s, "{days} {metric}");
128 }
129 if hours != 0 {
130 let metric = match hours {
131 1 => "hour",
132 _ => "hours",
133 };
134 if !out_s.is_empty() {
135 out_s.push_str(SEPARATOR);
136 }
137 let _ = write!(out_s, "{hours} {metric}");
138 }
139 if minutes != 0 {
140 let metric = match minutes {
141 1 => "minute",
142 _ => "minutes",
143 };
144 if !out_s.is_empty() {
145 out_s.push_str(SEPARATOR);
146 }
147 let _ = write!(out_s, "{minutes} {metric}");
148 }
149 if secs != 0 {
150 let metric = match secs {
151 1 => "second",
152 _ => "seconds",
153 };
154 if !out_s.is_empty() {
155 out_s.push_str(SEPARATOR);
156 }
157 let _ = write!(out_s, "{secs} {metric}");
158 }
159 Some(out_s)
160}
161
162#[cfg(test)]
163mod tests {
164 use crate::{
165 error::message::MessageError,
166 util::dates::{TIMESTAMP_FACTOR, format, get_local_time, get_offset, readable_diff},
167 };
168 use chrono::prelude::*;
169
170 #[test]
171 fn can_format_date_single_digit() {
172 let date = Local
173 .with_ymd_and_hms(2020, 5, 20, 9, 10, 11)
174 .single()
175 .ok_or(MessageError::InvalidTimestamp(0));
176 assert_eq!(format(&date), "May 20, 2020 9:10:11 AM");
177 }
178
179 #[test]
180 fn can_format_date_double_digit() {
181 let date = Local
182 .with_ymd_and_hms(2020, 5, 20, 10, 10, 11)
183 .single()
184 .ok_or(MessageError::InvalidTimestamp(0));
185 assert_eq!(format(&date), "May 20, 2020 10:10:11 AM");
186 }
187
188 #[test]
189 fn cant_format_diff_backwards() {
190 let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
191 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 30).unwrap());
192 assert_eq!(readable_diff(start, end), None);
193 }
194
195 #[test]
196 fn can_format_diff_all_singular() {
197 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
198 let end = Ok(Local.with_ymd_and_hms(2020, 5, 21, 10, 11, 12).unwrap());
199 assert_eq!(
200 readable_diff(start, end),
201 Some("1 day, 1 hour, 1 minute, 1 second".to_owned())
202 );
203 }
204
205 #[test]
206 fn can_format_diff_mixed_singular() {
207 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
208 let end = Ok(Local.with_ymd_and_hms(2020, 5, 22, 10, 20, 12).unwrap());
209 assert_eq!(
210 readable_diff(start, end),
211 Some("2 days, 1 hour, 10 minutes, 1 second".to_owned())
212 );
213 }
214
215 #[test]
216 fn can_format_diff_seconds() {
217 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
218 let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 30).unwrap());
219 assert_eq!(readable_diff(start, end), Some("19 seconds".to_owned()));
220 }
221
222 #[test]
223 fn can_format_diff_minutes() {
224 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
225 let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 15, 11).unwrap());
226 assert_eq!(readable_diff(start, end), Some("5 minutes".to_owned()));
227 }
228
229 #[test]
230 fn can_format_diff_hours() {
231 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
232 let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 12, 10, 11).unwrap());
233 assert_eq!(readable_diff(start, end), Some("3 hours".to_owned()));
234 }
235
236 #[test]
237 fn can_format_diff_days() {
238 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
239 let end = Ok(Local.with_ymd_and_hms(2020, 5, 30, 9, 10, 11).unwrap());
240 assert_eq!(readable_diff(start, end), Some("10 days".to_owned()));
241 }
242
243 #[test]
244 fn can_format_diff_minutes_seconds() {
245 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
246 let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 15, 30).unwrap());
247 assert_eq!(
248 readable_diff(start, end),
249 Some("5 minutes, 19 seconds".to_owned())
250 );
251 }
252
253 #[test]
254 fn can_format_diff_days_minutes() {
255 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
256 let end = Ok(Local.with_ymd_and_hms(2020, 5, 22, 9, 30, 11).unwrap());
257 assert_eq!(
258 readable_diff(start, end),
259 Some("2 days, 20 minutes".to_owned())
260 );
261 }
262
263 #[test]
264 fn can_format_diff_month() {
265 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
266 let end = Ok(Local.with_ymd_and_hms(2020, 7, 20, 9, 10, 11).unwrap());
267 assert_eq!(readable_diff(start, end), Some("61 days".to_owned()));
268 }
269
270 #[test]
271 fn can_format_diff_year() {
272 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
273 let end = Ok(Local.with_ymd_and_hms(2022, 7, 20, 9, 10, 11).unwrap());
274 assert_eq!(readable_diff(start, end), Some("791 days".to_owned()));
275 }
276
277 #[test]
278 fn can_format_diff_all() {
279 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
280 let end = Ok(Local.with_ymd_and_hms(2020, 5, 22, 14, 32, 45).unwrap());
281 assert_eq!(
282 readable_diff(start, end),
283 Some("2 days, 5 hours, 22 minutes, 34 seconds".to_owned())
284 );
285 }
286
287 #[test]
288 fn can_format_no_diff() {
289 let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
290 let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
291 assert_eq!(readable_diff(start, end), Some(String::new()));
292 }
293
294 #[test]
295 fn can_get_local_time_from_seconds_timestamp() {
296 let offset = get_offset();
297 let expected_utc = Utc
298 .with_ymd_and_hms(2020, 5, 20, 9, 10, 11)
299 .single()
300 .unwrap();
301
302 let stamp_secs = expected_utc.timestamp() - offset;
304
305 let local = get_local_time(&stamp_secs, &offset).unwrap();
306 let expected_local = expected_utc.with_timezone(&Local);
307
308 assert_eq!(local, expected_local);
309 }
310
311 #[test]
312 fn can_get_local_time_from_nanoseconds_timestamp() {
313 let offset = get_offset();
314 let expected_utc = Utc
315 .with_ymd_and_hms(2020, 5, 20, 9, 10, 11)
316 .single()
317 .unwrap();
318
319 let stamp_ns = (expected_utc.timestamp() - offset) * TIMESTAMP_FACTOR;
321
322 let local = get_local_time(&stamp_ns, &offset).unwrap();
323 let expected_local = expected_utc.with_timezone(&Local);
324
325 assert_eq!(local, expected_local);
326 }
327
328 #[test]
329 fn can_get_local_time_from_hardcoded_seconds_timestamp() {
330 let offset = get_offset();
331
332 let stamp_secs: i64 = 347_670_404;
334
335 let expected_utc = Utc.timestamp_opt(stamp_secs + offset, 0).single().unwrap();
336
337 let local = get_local_time(&stamp_secs, &offset).unwrap();
338 let expected_local = expected_utc.with_timezone(&Local);
339
340 assert_eq!(local, expected_local);
341 }
342
343 #[test]
344 fn can_get_local_time_from_hardcoded_nanoseconds_timestamp() {
345 let offset = get_offset();
346
347 let stamp_ns: i64 = 549_948_395_013_559_360;
349
350 let seconds_since_2001 = stamp_ns / TIMESTAMP_FACTOR;
351
352 let expected_utc = Utc
353 .timestamp_opt(seconds_since_2001 + offset, 0)
354 .single()
355 .unwrap();
356
357 let local = get_local_time(&stamp_ns, &offset).unwrap();
358 let expected_local = expected_utc.with_timezone(&Local);
359
360 assert_eq!(local, expected_local);
361 }
362}