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*/
6
7use chrono::{DateTime, Duration, Local, TimeZone, Utc};
8
9use crate::error::message::MessageError;
10
11const SEPARATOR: &str = ", ";
12pub const TIMESTAMP_FACTOR: i64 = 1000000000;
13
14/// Get the date offset for the iMessage Database
15///
16/// This offset is used to adjust the unix timestamps stored in the iMessage database
17/// with a non-standard epoch of `2001-01-01 00:00:00` in the current machine's local time zone.
18///
19/// # Example
20///
21/// ```
22/// use imessage_database::util::dates::get_offset;
23///
24/// let current_epoch = get_offset();
25/// ```
26pub fn get_offset() -> i64 {
27    Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0)
28        .unwrap()
29        .timestamp()
30}
31
32/// Create a `DateTime<Local>` from an arbitrary date and offset
33///
34/// This is used to create date data for anywhere dates are stored in the table, including
35/// `PLIST` payloads or [`typedstream`](crate::util::typedstream) data.
36///
37/// # Example
38///
39/// ```
40/// use imessage_database::util::dates::{get_local_time, get_offset};
41///
42/// let current_offset = get_offset();
43/// let local = get_local_time(&674526582885055488, &current_offset).unwrap();
44/// ```
45pub fn get_local_time(date_stamp: &i64, offset: &i64) -> Result<DateTime<Local>, MessageError> {
46    let utc_stamp = DateTime::from_timestamp((date_stamp / TIMESTAMP_FACTOR) + offset, 0)
47        .ok_or(MessageError::InvalidTimestamp(*date_stamp))?
48        .naive_utc();
49    Ok(Local.from_utc_datetime(&utc_stamp))
50}
51
52/// Format a date from the iMessage table for reading
53///
54/// # Example:
55///
56/// ```
57/// use chrono::offset::Local;
58/// use imessage_database::util::dates::format;
59///
60/// let date = format(&Ok(Local::now()));
61/// println!("{date}");
62/// ```
63pub fn format(date: &Result<DateTime<Local>, MessageError>) -> String {
64    match date {
65        Ok(d) => DateTime::format(d, "%b %d, %Y %l:%M:%S %p").to_string(),
66        Err(why) => why.to_string(),
67    }
68}
69
70/// Generate a readable diff from two local timestamps.
71///
72/// # Example:
73///
74/// ```
75/// use chrono::prelude::*;
76/// use imessage_database::util::dates::readable_diff;
77///
78/// let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
79/// let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 15, 13).unwrap());
80/// println!("{}", readable_diff(start, end).unwrap()) // "5 minutes, 2 seconds"
81/// ```
82pub fn readable_diff(
83    start: Result<DateTime<Local>, MessageError>,
84    end: Result<DateTime<Local>, MessageError>,
85) -> Option<String> {
86    // Calculate diff
87    let diff: Duration = end.ok()? - start.ok()?;
88    let seconds = diff.num_seconds();
89
90    // Early escape for invalid date diff
91    if seconds < 0 {
92        return None;
93    }
94
95    // 42 is the length of a diff string that has all components with 2 digits each
96    // This allocation improved performance over `::new()` by 20%
97    // (21.99s to 27.79s over 250k messages)
98    let mut out_s = String::with_capacity(42);
99
100    let days = seconds / 86400;
101    let hours = (seconds % 86400) / 3600;
102    let minutes = (seconds % 86400 % 3600) / 60;
103    let secs = seconds % 86400 % 3600 % 60;
104
105    if days != 0 {
106        let metric = match days {
107            1 => "day",
108            _ => "days",
109        };
110        out_s.push_str(&format!("{days} {metric}"));
111    }
112    if hours != 0 {
113        let metric = match hours {
114            1 => "hour",
115            _ => "hours",
116        };
117        if !out_s.is_empty() {
118            out_s.push_str(SEPARATOR);
119        }
120        out_s.push_str(&format!("{hours} {metric}"));
121    }
122    if minutes != 0 {
123        let metric = match minutes {
124            1 => "minute",
125            _ => "minutes",
126        };
127        if !out_s.is_empty() {
128            out_s.push_str(SEPARATOR);
129        }
130        out_s.push_str(&format!("{minutes} {metric}"));
131    }
132    if secs != 0 {
133        let metric = match secs {
134            1 => "second",
135            _ => "seconds",
136        };
137        if !out_s.is_empty() {
138            out_s.push_str(SEPARATOR);
139        }
140        out_s.push_str(&format!("{secs} {metric}"));
141    }
142    Some(out_s)
143}
144
145#[cfg(test)]
146mod tests {
147    use crate::{
148        error::message::MessageError,
149        util::dates::{format, readable_diff},
150    };
151    use chrono::prelude::*;
152
153    #[test]
154    fn can_format_date_single_digit() {
155        let date = Local
156            .with_ymd_and_hms(2020, 5, 20, 9, 10, 11)
157            .single()
158            .ok_or(MessageError::InvalidTimestamp(0));
159        assert_eq!(format(&date), "May 20, 2020  9:10:11 AM");
160    }
161
162    #[test]
163    fn can_format_date_double_digit() {
164        let date = Local
165            .with_ymd_and_hms(2020, 5, 20, 10, 10, 11)
166            .single()
167            .ok_or(MessageError::InvalidTimestamp(0));
168        assert_eq!(format(&date), "May 20, 2020 10:10:11 AM");
169    }
170
171    #[test]
172    fn cant_format_diff_backwards() {
173        let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
174        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 30).unwrap());
175        assert_eq!(readable_diff(start, end), None);
176    }
177
178    #[test]
179    fn can_format_diff_all_singular() {
180        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
181        let end = Ok(Local.with_ymd_and_hms(2020, 5, 21, 10, 11, 12).unwrap());
182        assert_eq!(
183            readable_diff(start, end),
184            Some("1 day, 1 hour, 1 minute, 1 second".to_owned())
185        );
186    }
187
188    #[test]
189    fn can_format_diff_mixed_singular() {
190        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
191        let end = Ok(Local.with_ymd_and_hms(2020, 5, 22, 10, 20, 12).unwrap());
192        assert_eq!(
193            readable_diff(start, end),
194            Some("2 days, 1 hour, 10 minutes, 1 second".to_owned())
195        );
196    }
197
198    #[test]
199    fn can_format_diff_seconds() {
200        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
201        let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 30).unwrap());
202        assert_eq!(readable_diff(start, end), Some("19 seconds".to_owned()));
203    }
204
205    #[test]
206    fn can_format_diff_minutes() {
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, 20, 9, 15, 11).unwrap());
209        assert_eq!(readable_diff(start, end), Some("5 minutes".to_owned()));
210    }
211
212    #[test]
213    fn can_format_diff_hours() {
214        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
215        let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 12, 10, 11).unwrap());
216        assert_eq!(readable_diff(start, end), Some("3 hours".to_owned()));
217    }
218
219    #[test]
220    fn can_format_diff_days() {
221        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
222        let end = Ok(Local.with_ymd_and_hms(2020, 5, 30, 9, 10, 11).unwrap());
223        assert_eq!(readable_diff(start, end), Some("10 days".to_owned()));
224    }
225
226    #[test]
227    fn can_format_diff_minutes_seconds() {
228        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
229        let end = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 15, 30).unwrap());
230        assert_eq!(
231            readable_diff(start, end),
232            Some("5 minutes, 19 seconds".to_owned())
233        );
234    }
235
236    #[test]
237    fn can_format_diff_days_minutes() {
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, 22, 9, 30, 11).unwrap());
240        assert_eq!(
241            readable_diff(start, end),
242            Some("2 days, 20 minutes".to_owned())
243        );
244    }
245
246    #[test]
247    fn can_format_diff_month() {
248        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
249        let end = Ok(Local.with_ymd_and_hms(2020, 7, 20, 9, 10, 11).unwrap());
250        assert_eq!(readable_diff(start, end), Some("61 days".to_owned()));
251    }
252
253    #[test]
254    fn can_format_diff_year() {
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(2022, 7, 20, 9, 10, 11).unwrap());
257        assert_eq!(readable_diff(start, end), Some("791 days".to_owned()));
258    }
259
260    #[test]
261    fn can_format_diff_all() {
262        let start = Ok(Local.with_ymd_and_hms(2020, 5, 20, 9, 10, 11).unwrap());
263        let end = Ok(Local.with_ymd_and_hms(2020, 5, 22, 14, 32, 45).unwrap());
264        assert_eq!(
265            readable_diff(start, end),
266            Some("2 days, 5 hours, 22 minutes, 34 seconds".to_owned())
267        );
268    }
269
270    #[test]
271    fn can_format_no_diff() {
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(2020, 5, 20, 9, 10, 11).unwrap());
274        assert_eq!(readable_diff(start, end), Some("".to_owned()));
275    }
276}