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