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