1use std::fmt::Write;
7
8use chrono::{DateTime, Datelike, 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
23pub const MESSAGES_EPOCH_UNIX_SECS: i64 = 978_307_200;
25
26pub fn to_utc(stamp: i64) -> Result<DateTime<Utc>, MessageError> {
42 let seconds_since_2001 = if stamp >= 1_000_000_000_000 {
45 stamp / TIMESTAMP_FACTOR
46 } else {
47 stamp
48 };
49
50 DateTime::from_timestamp(seconds_since_2001 + MESSAGES_EPOCH_UNIX_SECS, 0)
51 .ok_or(MessageError::InvalidTimestamp(stamp))
52}
53
54#[must_use]
66pub fn format<Tz: TimeZone>(date: &DateTime<Tz>) -> String {
67 const MONTHS: [&str; 12] = [
70 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
71 ];
72 let (hour12, meridiem) = match date.hour() {
73 0 => (12, "AM"),
74 12 => (12, "PM"),
75 h if h < 12 => (h, "AM"),
76 h => (h - 12, "PM"),
77 };
78
79 let mut out = String::with_capacity(24);
80 let _ = write!(
81 out,
82 "{} {:02}, {} {hour12:2}:{:02}:{:02} {meridiem}",
83 MONTHS[(date.month() - 1) as usize],
84 date.day(),
85 date.year(),
86 date.minute(),
87 date.second(),
88 );
89 out
90}
91
92#[must_use]
108pub fn readable_diff<Tz: TimeZone>(start: &DateTime<Tz>, end: &DateTime<Tz>) -> Option<String> {
109 let seconds = end.timestamp() - start.timestamp();
110
111 if seconds < 0 {
112 return None;
113 }
114
115 let (years, remaining_seconds) = years_and_remainder(start, end)
116 .unwrap_or((seconds / SECONDS_PER_YEAR, seconds % SECONDS_PER_YEAR));
117
118 let mut out_s = String::with_capacity(51);
120
121 let days = remaining_seconds / SECONDS_PER_DAY;
122 let hours = (remaining_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
123 let minutes = (remaining_seconds % SECONDS_PER_DAY % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
124 let secs = remaining_seconds % SECONDS_PER_DAY % SECONDS_PER_HOUR % SECONDS_PER_MINUTE;
125
126 append_component(&mut out_s, years, "year", "years");
127 append_component(&mut out_s, days, "day", "days");
128 append_component(&mut out_s, hours, "hour", "hours");
129 append_component(&mut out_s, minutes, "minute", "minutes");
130 append_component(&mut out_s, secs, "second", "seconds");
131
132 Some(out_s)
133}
134
135fn years_and_remainder<Tz: TimeZone>(
137 start: &DateTime<Tz>,
138 end: &DateTime<Tz>,
139) -> Option<(i64, i64)> {
140 let mut years = end.year() - start.year();
141
142 if years <= 0 {
143 return Some((0, end.timestamp() - start.timestamp()));
144 }
145
146 let mut remainder_start = start
149 .clone()
150 .checked_add_months(Months::new(u32::try_from(years).ok()?.checked_mul(12)?))?;
151
152 if remainder_start > *end {
153 years -= 1;
154 remainder_start = start
155 .clone()
156 .checked_add_months(Months::new(u32::try_from(years).ok()?.checked_mul(12)?))?;
157 }
158
159 Some((
160 i64::from(years),
161 end.timestamp() - remainder_start.timestamp(),
162 ))
163}
164
165fn append_component(out_s: &mut String, value: i64, singular: &str, plural: &str) {
167 if value == 0 {
168 return;
169 }
170
171 if !out_s.is_empty() {
172 out_s.push_str(SEPARATOR);
173 }
174
175 let metric = if value == 1 { singular } else { plural };
176 let _ = write!(out_s, "{value} {metric}");
177}
178
179#[cfg(test)]
180mod tests {
181 use crate::util::dates::{
182 MESSAGES_EPOCH_UNIX_SECS, TIMESTAMP_FACTOR, format, readable_diff, to_utc,
183 };
184 use chrono::prelude::*;
185
186 #[test]
187 fn can_format_date_single_digit() {
188 let date = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
189 assert_eq!(format(&date), "May 20, 2020 9:10:11 AM");
190 }
191
192 #[test]
193 fn can_format_date_double_digit() {
194 let date = Utc.with_ymd_and_hms(2020, 5, 20, 10, 10, 11).unwrap();
195 assert_eq!(format(&date), "May 20, 2020 10:10:11 AM");
196 }
197
198 #[test]
199 fn can_format_date_midnight() {
200 let date = Utc.with_ymd_and_hms(2020, 5, 20, 0, 5, 9).unwrap();
202 assert_eq!(format(&date), "May 20, 2020 12:05:09 AM");
203 }
204
205 #[test]
206 fn can_format_date_noon() {
207 let date = Utc.with_ymd_and_hms(2020, 5, 20, 12, 0, 0).unwrap();
209 assert_eq!(format(&date), "May 20, 2020 12:00:00 PM");
210 }
211
212 #[test]
213 fn can_format_date_afternoon() {
214 let date = Utc.with_ymd_and_hms(2020, 5, 20, 15, 4, 5).unwrap();
216 assert_eq!(format(&date), "May 20, 2020 3:04:05 PM");
217 }
218
219 #[test]
220 fn can_format_date_in_fixed_offset_wall_clock() {
221 let instant = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
224 let zoned = instant.with_timezone(&FixedOffset::east_opt(5 * 3600 + 30 * 60).unwrap());
225 assert_eq!(format(&zoned), "May 20, 2020 2:40:11 PM");
226 }
227
228 #[test]
229 fn cant_format_diff_backwards() {
230 let end = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
231 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 30).unwrap();
232 assert_eq!(readable_diff(&start, &end), None);
233 }
234
235 #[test]
236 fn can_format_diff_all_singular() {
237 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
238 let end = Utc.with_ymd_and_hms(2020, 5, 21, 10, 11, 12).unwrap();
239 assert_eq!(
240 readable_diff(&start, &end),
241 Some("1 day, 1 hour, 1 minute, 1 second".to_owned())
242 );
243 }
244
245 #[test]
246 fn can_format_diff_mixed_singular() {
247 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
248 let end = Utc.with_ymd_and_hms(2020, 5, 22, 10, 20, 12).unwrap();
249 assert_eq!(
250 readable_diff(&start, &end),
251 Some("2 days, 1 hour, 10 minutes, 1 second".to_owned())
252 );
253 }
254
255 #[test]
256 fn can_format_diff_components() {
257 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
258 let cases = [
259 ("seconds", (2020, 5, 20, 9, 10, 30), "19 seconds"),
260 ("minutes", (2020, 5, 20, 9, 15, 11), "5 minutes"),
261 ("hours", (2020, 5, 20, 12, 10, 11), "3 hours"),
262 ("days", (2020, 5, 30, 9, 10, 11), "10 days"),
263 (
264 "minutes and seconds",
265 (2020, 5, 20, 9, 15, 30),
266 "5 minutes, 19 seconds",
267 ),
268 (
269 "days and minutes",
270 (2020, 5, 22, 9, 30, 11),
271 "2 days, 20 minutes",
272 ),
273 (
274 "every component",
275 (2020, 5, 22, 14, 32, 45),
276 "2 days, 5 hours, 22 minutes, 34 seconds",
277 ),
278 ];
279 for (label, (year, month, day, hour, minute, second), expected) in cases {
280 let end = Utc
281 .with_ymd_and_hms(year, month, day, hour, minute, second)
282 .unwrap();
283 assert_eq!(
284 readable_diff(&start, &end),
285 Some(expected.to_owned()),
286 "{label}"
287 );
288 }
289 }
290
291 #[test]
292 fn can_format_diff_month() {
293 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
294 let end = Utc.with_ymd_and_hms(2020, 7, 20, 9, 10, 11).unwrap();
295 assert_eq!(readable_diff(&start, &end), Some("61 days".to_owned()));
296 }
297
298 #[test]
299 fn can_format_diff_single_year() {
300 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
301 let end = Utc.with_ymd_and_hms(2021, 5, 20, 9, 10, 11).unwrap();
302 assert_eq!(readable_diff(&start, &end), Some("1 year".to_owned()));
303 }
304
305 #[test]
306 fn can_format_diff_years_days() {
307 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
308 let end = Utc.with_ymd_and_hms(2022, 7, 20, 9, 10, 11).unwrap();
309 assert_eq!(
310 readable_diff(&start, &end),
311 Some("2 years, 61 days".to_owned())
312 );
313 }
314
315 #[test]
316 fn can_format_diff_leap_day_anniversary_as_year() {
317 let start = Utc.with_ymd_and_hms(2020, 2, 29, 9, 10, 11).unwrap();
318 let end = Utc.with_ymd_and_hms(2021, 2, 28, 9, 10, 11).unwrap();
319 assert_eq!(readable_diff(&start, &end), Some("1 year".to_owned()));
320 }
321
322 #[test]
323 fn can_format_no_diff() {
324 let start = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
325 let end = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
326 assert_eq!(readable_diff(&start, &end), Some(String::new()));
327 }
328
329 #[test]
330 fn messages_epoch_is_2001_01_01_utc() {
331 assert_eq!(
332 MESSAGES_EPOCH_UNIX_SECS,
333 Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0)
334 .unwrap()
335 .timestamp()
336 );
337 }
338
339 #[test]
340 fn can_decode_seconds_timestamp() {
341 let expected = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
342
343 let stamp_secs = expected.timestamp() - MESSAGES_EPOCH_UNIX_SECS;
345
346 assert_eq!(to_utc(stamp_secs).unwrap(), expected);
347 }
348
349 #[test]
350 fn can_decode_nanoseconds_timestamp() {
351 let expected = Utc.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap();
352
353 let stamp_ns = (expected.timestamp() - MESSAGES_EPOCH_UNIX_SECS) * TIMESTAMP_FACTOR;
355
356 assert_eq!(to_utc(stamp_ns).unwrap(), expected);
357 }
358}