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