imessage_database/util/
dates.rs

1/*!
2 Contains date parsing functions for iMessage dates.
3
4 Most dates are stored as nanosecond-precision unix timestamps with an epoch of `1/1/2001 00:00:00` in the local time zone.
5*/
6use std::fmt::Write;
7
8use chrono::{DateTime, Duration, Local, TimeZone, Utc};
9
10use crate::error::message::MessageError;
11
12const SEPARATOR: &str = ", ";
13
14/// Factor used to convert between nanosecond-precision timestamps and seconds
15///
16/// The iMessage database stores timestamps as nanoseconds, so this factor is used
17/// to convert between the database format and standard Unix timestamps.
18pub const TIMESTAMP_FACTOR: i64 = 1_000_000_000;
19
20/// Get the date offset for the iMessage Database
21///
22/// This offset is used to adjust the unix timestamps stored in the iMessage database
23/// with a non-standard epoch of `2001-01-01 00:00:00` in the current machine's local time zone.
24///
25/// # Example
26///
27/// ```
28/// use imessage_database::util::dates::get_offset;
29///
30/// let current_epoch = get_offset();
31/// ```
32#[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
39/// Create a `DateTime<Local>` from an arbitrary date and offset
40///
41/// This is used to create date data for anywhere dates are stored in the table, including
42/// `PLIST` payloads or [`typedstream`](crate::util::typedstream) data.
43///
44/// # Example
45///
46/// ```
47/// use imessage_database::util::dates::{get_local_time, get_offset};
48///
49/// let current_offset = get_offset();
50/// let local = get_local_time(&674526582885055488, &current_offset).unwrap();
51/// ```
52pub fn get_local_time(date_stamp: &i64, offset: &i64) -> Result<DateTime<Local>, MessageError> {
53    // Newer databases store timestamps as nanoseconds since 2001-01-01,
54    // while older ones store plain seconds since 2001-01-01.
55    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/// Format a date from the iMessage table for reading
68///
69/// # Example:
70///
71/// ```
72/// use chrono::offset::Local;
73/// use imessage_database::util::dates::format;
74///
75/// let date = format(&Ok(Local::now()));
76/// println!("{date}");
77/// ```
78#[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/// Generate a readable diff from two local timestamps.
87///
88/// # Example:
89///
90/// ```
91/// use chrono::prelude::*;
92/// use imessage_database::util::dates::readable_diff;
93///
94/// let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
95/// let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 15, 13).unwrap());
96/// println!("{}", readable_diff(start, end).unwrap()) // "5 minutes, 2 seconds"
97/// ```
98#[must_use]
99pub fn readable_diff(
100    start: Result<DateTime<Local>, MessageError>,
101    end: Result<DateTime<Local>, MessageError>,
102) -> Option<String> {
103    // Calculate diff
104    let diff: Duration = end.ok()? - start.ok()?;
105    let seconds = diff.num_seconds();
106
107    // Early escape for invalid date diff
108    if seconds < 0 {
109        return None;
110    }
111
112    // 42 is the length of a diff string that has all components with 2 digits each
113    // This allocation improved performance over `::new()` by 20%
114    // (21.99s to 27.79s over 250k messages)
115    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        // Older databases store seconds since 2001-01-01 00:00:00
303        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        // Newer databases store nanoseconds since 2001-01-01 00:00:00
320        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        // Legacy-style seconds timestamp
333        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        // Nanosecond-style timestamp
348        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}