1use std::fmt::Write;
7
8use chrono::{DateTime, Datelike, Local, Months, TimeZone, Timelike, 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;
22
23#[must_use]
35pub fn get_offset() -> i64 {
36 Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0)
37 .unwrap()
38 .timestamp()
39}
40
41pub fn get_local_time(date_stamp: i64, offset: i64) -> Result<DateTime<Local>, MessageError> {
54 let seconds_since_2001 = if date_stamp >= 1_000_000_000_000 {
57 date_stamp / TIMESTAMP_FACTOR
58 } else {
59 date_stamp
60 };
61
62 let utc_stamp = DateTime::from_timestamp(seconds_since_2001 + offset, 0)
63 .ok_or(MessageError::InvalidTimestamp(date_stamp))?
64 .naive_utc();
65 Ok(Local.from_utc_datetime(&utc_stamp))
66}
67
68#[must_use]
80pub fn format(date: &DateTime<Local>) -> String {
81 const MONTHS: [&str; 12] = [
84 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
85 ];
86 let (hour12, meridiem) = match date.hour() {
87 0 => (12, "AM"),
88 12 => (12, "PM"),
89 h if h < 12 => (h, "AM"),
90 h => (h - 12, "PM"),
91 };
92
93 let mut out = String::with_capacity(24);
94 let _ = write!(
95 out,
96 "{} {:02}, {} {hour12:2}:{:02}:{:02} {meridiem}",
97 MONTHS[(date.month() - 1) as usize],
98 date.day(),
99 date.year(),
100 date.minute(),
101 date.second(),
102 );
103 out
104}
105
106#[must_use]
119pub fn readable_diff(start: &DateTime<Local>, end: &DateTime<Local>) -> Option<String> {
120 let seconds = end.timestamp() - start.timestamp();
121
122 if seconds < 0 {
123 return None;
124 }
125
126 let (years, remaining_seconds) = years_and_remainder(start, end)
127 .unwrap_or((seconds / SECONDS_PER_YEAR, seconds % SECONDS_PER_YEAR));
128
129 let mut out_s = String::with_capacity(51);
131
132 let days = remaining_seconds / SECONDS_PER_DAY;
133 let hours = (remaining_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
134 let minutes = (remaining_seconds % SECONDS_PER_DAY % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
135 let secs = remaining_seconds % SECONDS_PER_DAY % SECONDS_PER_HOUR % SECONDS_PER_MINUTE;
136
137 append_component(&mut out_s, years, "year", "years");
138 append_component(&mut out_s, days, "day", "days");
139 append_component(&mut out_s, hours, "hour", "hours");
140 append_component(&mut out_s, minutes, "minute", "minutes");
141 append_component(&mut out_s, secs, "second", "seconds");
142
143 Some(out_s)
144}
145
146fn years_and_remainder(start: &DateTime<Local>, end: &DateTime<Local>) -> Option<(i64, i64)> {
148 let mut years = end.year() - start.year();
149
150 if years <= 0 {
151 return Some((0, end.timestamp() - start.timestamp()));
152 }
153
154 let mut remainder_start =
155 start.checked_add_months(Months::new(u32::try_from(years).ok()?.checked_mul(12)?))?;
156
157 if remainder_start > *end {
158 years -= 1;
159 remainder_start =
160 start.checked_add_months(Months::new(u32::try_from(years).ok()?.checked_mul(12)?))?;
161 }
162
163 Some((
164 i64::from(years),
165 end.timestamp() - remainder_start.timestamp(),
166 ))
167}
168
169fn append_component(out_s: &mut String, value: i64, singular: &str, plural: &str) {
171 if value == 0 {
172 return;
173 }
174
175 if !out_s.is_empty() {
176 out_s.push_str(SEPARATOR);
177 }
178
179 let metric = if value == 1 { singular } else { plural };
180 let _ = write!(out_s, "{value} {metric}");
181}
182
183#[cfg(test)]
184mod tests {
185 use crate::util::dates::{TIMESTAMP_FACTOR, format, get_local_time, get_offset, readable_diff};
186 use chrono::prelude::*;
187
188 #[test]
189 fn can_format_date_single_digit() {
190 let date = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
191 assert_eq!(format(&date), "May 20, 2020 9:10:11 AM");
192 }
193
194 #[test]
195 fn can_format_date_double_digit() {
196 let date = Local.with_ymd_and_hms(2020, 5, 20, 10, 10, 11).unwrap();
197 assert_eq!(format(&date), "May 20, 2020 10:10:11 AM");
198 }
199
200 #[test]
201 fn can_format_date_midnight() {
202 let date = Local.with_ymd_and_hms(2020, 5, 20, 0, 5, 9).unwrap();
204 assert_eq!(format(&date), "May 20, 2020 12:05:09 AM");
205 }
206
207 #[test]
208 fn can_format_date_noon() {
209 let date = Local.with_ymd_and_hms(2020, 5, 20, 12, 0, 0).unwrap();
211 assert_eq!(format(&date), "May 20, 2020 12:00:00 PM");
212 }
213
214 #[test]
215 fn can_format_date_afternoon() {
216 let date = Local.with_ymd_and_hms(2020, 5, 20, 15, 4, 5).unwrap();
218 assert_eq!(format(&date), "May 20, 2020 3:04:05 PM");
219 }
220
221 #[test]
222 fn cant_format_diff_backwards() {
223 let end = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
224 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 30).unwrap();
225 assert_eq!(readable_diff(&start, &end), None);
226 }
227
228 #[test]
229 fn can_format_diff_all_singular() {
230 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
231 let end = Local.with_ymd_and_hms(2020, 5, 21, 10, 11, 12).unwrap();
232 assert_eq!(
233 readable_diff(&start, &end),
234 Some("1 day, 1 hour, 1 minute, 1 second".to_owned())
235 );
236 }
237
238 #[test]
239 fn can_format_diff_mixed_singular() {
240 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
241 let end = Local.with_ymd_and_hms(2020, 5, 22, 10, 20, 12).unwrap();
242 assert_eq!(
243 readable_diff(&start, &end),
244 Some("2 days, 1 hour, 10 minutes, 1 second".to_owned())
245 );
246 }
247
248 #[test]
249 fn can_format_diff_seconds() {
250 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
251 let end = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 30).unwrap();
252 assert_eq!(readable_diff(&start, &end), Some("19 seconds".to_owned()));
253 }
254
255 #[test]
256 fn can_format_diff_minutes() {
257 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
258 let end = Local.with_ymd_and_hms(2020, 5, 20, 9, 15, 11).unwrap();
259 assert_eq!(readable_diff(&start, &end), Some("5 minutes".to_owned()));
260 }
261
262 #[test]
263 fn can_format_diff_hours() {
264 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
265 let end = Local.with_ymd_and_hms(2020, 5, 20, 12, 10, 11).unwrap();
266 assert_eq!(readable_diff(&start, &end), Some("3 hours".to_owned()));
267 }
268
269 #[test]
270 fn can_format_diff_days() {
271 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
272 let end = Local.with_ymd_and_hms(2020, 5, 30, 9, 10, 11).unwrap();
273 assert_eq!(readable_diff(&start, &end), Some("10 days".to_owned()));
274 }
275
276 #[test]
277 fn can_format_diff_minutes_seconds() {
278 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
279 let end = Local.with_ymd_and_hms(2020, 5, 20, 9, 15, 30).unwrap();
280 assert_eq!(
281 readable_diff(&start, &end),
282 Some("5 minutes, 19 seconds".to_owned())
283 );
284 }
285
286 #[test]
287 fn can_format_diff_days_minutes() {
288 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
289 let end = Local.with_ymd_and_hms(2020, 5, 22, 9, 30, 11).unwrap();
290 assert_eq!(
291 readable_diff(&start, &end),
292 Some("2 days, 20 minutes".to_owned())
293 );
294 }
295
296 #[test]
297 fn can_format_diff_month() {
298 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
299 let end = Local.with_ymd_and_hms(2020, 7, 20, 9, 10, 11).unwrap();
300 assert_eq!(readable_diff(&start, &end), Some("61 days".to_owned()));
301 }
302
303 #[test]
304 fn can_format_diff_single_year() {
305 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
306 let end = Local.with_ymd_and_hms(2021, 5, 20, 9, 10, 11).unwrap();
307 assert_eq!(readable_diff(&start, &end), Some("1 year".to_owned()));
308 }
309
310 #[test]
311 fn can_format_diff_years_days() {
312 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
313 let end = Local.with_ymd_and_hms(2022, 7, 20, 9, 10, 11).unwrap();
314 assert_eq!(
315 readable_diff(&start, &end),
316 Some("2 years, 61 days".to_owned())
317 );
318 }
319
320 #[test]
321 fn can_format_diff_leap_day_anniversary_as_year() {
322 let start = Local.with_ymd_and_hms(2020, 2, 29, 9, 10, 11).unwrap();
323 let end = Local.with_ymd_and_hms(2021, 2, 28, 9, 10, 11).unwrap();
324 assert_eq!(readable_diff(&start, &end), Some("1 year".to_owned()));
325 }
326
327 #[test]
328 fn can_format_diff_all() {
329 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
330 let end = Local.with_ymd_and_hms(2020, 5, 22, 14, 32, 45).unwrap();
331 assert_eq!(
332 readable_diff(&start, &end),
333 Some("2 days, 5 hours, 22 minutes, 34 seconds".to_owned())
334 );
335 }
336
337 #[test]
338 fn can_format_no_diff() {
339 let start = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
340 let end = Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
341 assert_eq!(readable_diff(&start, &end), Some(String::new()));
342 }
343
344 #[test]
345 fn can_get_local_time_from_seconds_timestamp() {
346 let offset = get_offset();
347 let expected_utc = Utc
348 .with_ymd_and_hms(2020, 5, 20, 9, 10, 11)
349 .single()
350 .unwrap();
351
352 let stamp_secs = expected_utc.timestamp() - offset;
354
355 let local = get_local_time(stamp_secs, offset).unwrap();
356 let expected_local = expected_utc.with_timezone(&Local);
357
358 assert_eq!(local, expected_local);
359 }
360
361 #[test]
362 fn can_get_local_time_from_nanoseconds_timestamp() {
363 let offset = get_offset();
364 let expected_utc = Utc
365 .with_ymd_and_hms(2020, 5, 20, 9, 10, 11)
366 .single()
367 .unwrap();
368
369 let stamp_ns = (expected_utc.timestamp() - offset) * TIMESTAMP_FACTOR;
371
372 let local = get_local_time(stamp_ns, offset).unwrap();
373 let expected_local = expected_utc.with_timezone(&Local);
374
375 assert_eq!(local, expected_local);
376 }
377
378 #[test]
379 fn can_get_local_time_from_hardcoded_seconds_timestamp() {
380 let offset = get_offset();
381
382 let stamp_secs: i64 = 347_670_404;
384
385 let expected_utc = Utc.timestamp_opt(stamp_secs + offset, 0).single().unwrap();
386
387 let local = get_local_time(stamp_secs, offset).unwrap();
388 let expected_local = expected_utc.with_timezone(&Local);
389
390 assert_eq!(local, expected_local);
391 }
392
393 #[test]
394 fn can_get_local_time_from_hardcoded_nanoseconds_timestamp() {
395 let offset = get_offset();
396
397 let stamp_ns: i64 = 549_948_395_013_559_360;
399
400 let seconds_since_2001 = stamp_ns / TIMESTAMP_FACTOR;
401
402 let expected_utc = Utc
403 .timestamp_opt(seconds_since_2001 + offset, 0)
404 .single()
405 .unwrap();
406
407 let local = get_local_time(stamp_ns, offset).unwrap();
408 let expected_local = expected_utc.with_timezone(&Local);
409
410 assert_eq!(local, expected_local);
411 }
412}