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