format_distance.rs

  1use chrono::{DateTime, Local, NaiveDateTime};
  2
  3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  4pub enum DateTimeType {
  5    Naive(NaiveDateTime),
  6    Local(DateTime<Local>),
  7}
  8
  9impl DateTimeType {
 10    /// Converts the DateTimeType to a NaiveDateTime.
 11    ///
 12    /// If the DateTimeType is already a NaiveDateTime, it will be returned as is.
 13    /// If the DateTimeType is a DateTime<Local>, it will be converted to a NaiveDateTime.
 14    pub fn to_naive(&self) -> NaiveDateTime {
 15        match self {
 16            DateTimeType::Naive(naive) => *naive,
 17            DateTimeType::Local(local) => local.naive_local(),
 18        }
 19    }
 20}
 21
 22/// Calculates the distance in seconds between two NaiveDateTime objects.
 23/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
 24///
 25/// ## Arguments
 26///
 27/// * `date` - A NaiveDateTime object representing the date of interest
 28/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made
 29fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
 30    let duration = date.signed_duration_since(base_date);
 31    -duration.num_seconds()
 32}
 33
 34/// Generates a string describing the time distance between two dates in a human-readable way.
 35fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
 36    let suffix = if distance < 0 { " from now" } else { " ago" };
 37
 38    let distance = distance.abs();
 39
 40    let minutes = distance / 60;
 41    let hours = distance / 3_600;
 42    let days = distance / 86_400;
 43    let months = distance / 2_592_000;
 44
 45    let string = if distance < 5 && include_seconds {
 46        "less than 5 seconds".to_string()
 47    } else if distance < 10 && include_seconds {
 48        "less than 10 seconds".to_string()
 49    } else if distance < 20 && include_seconds {
 50        "less than 20 seconds".to_string()
 51    } else if distance < 40 && include_seconds {
 52        "half a minute".to_string()
 53    } else if distance < 60 && include_seconds {
 54        "less than a minute".to_string()
 55    } else if distance < 90 && include_seconds {
 56        "1 minute".to_string()
 57    } else if distance < 30 {
 58        "less than a minute".to_string()
 59    } else if distance < 90 {
 60        "1 minute".to_string()
 61    } else if distance < 2_700 {
 62        format!("{} minutes", minutes)
 63    } else if distance < 5_400 {
 64        "about 1 hour".to_string()
 65    } else if distance < 86_400 {
 66        format!("about {} hours", hours)
 67    } else if distance < 172_800 {
 68        "1 day".to_string()
 69    } else if distance < 2_592_000 {
 70        format!("{} days", days)
 71    } else if distance < 5_184_000 {
 72        "about 1 month".to_string()
 73    } else if distance < 7_776_000 {
 74        "about 2 months".to_string()
 75    } else if distance < 31_540_000 {
 76        format!("{} months", months)
 77    } else if distance < 39_425_000 {
 78        "about 1 year".to_string()
 79    } else if distance < 55_195_000 {
 80        "over 1 year".to_string()
 81    } else if distance < 63_080_000 {
 82        "almost 2 years".to_string()
 83    } else {
 84        let years = distance / 31_536_000;
 85        let remaining_months = (distance % 31_536_000) / 2_592_000;
 86
 87        if remaining_months < 3 {
 88            format!("about {} years", years)
 89        } else if remaining_months < 9 {
 90            format!("over {} years", years)
 91        } else {
 92            format!("almost {} years", years + 1)
 93        }
 94    };
 95
 96    if add_suffix {
 97        format!("{}{}", string, suffix)
 98    } else {
 99        string
100    }
101}
102
103/// Get the time difference between two dates into a relative human readable string.
104///
105/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
106///
107/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now.
108///
109/// # Arguments
110///
111/// * `date` - The NaiveDateTime to compare.
112/// * `base_date` - The NaiveDateTime to compare against.
113/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
114/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
115///
116/// # Example
117///
118/// ```rust
119/// use chrono::DateTime;
120/// use ui2::utils::naive_format_distance;
121///
122/// fn time_between_moon_landings() -> String {
123///     let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
124///     let base_date = DateTime::parse_from_rfc3339("1972-12-14T00:00:00Z").unwrap().naive_local();
125///     format!("There was {} between the first and last crewed moon landings.", naive_format_distance(date, base_date, false, false))
126/// }
127/// ```
128///
129/// Output: `"There was about 3 years between the first and last crewed moon landings."`
130pub fn format_distance(
131    date: DateTimeType,
132    base_date: NaiveDateTime,
133    include_seconds: bool,
134    add_suffix: bool,
135) -> String {
136    let distance = distance_in_seconds(date.to_naive(), base_date);
137
138    distance_string(distance, include_seconds, add_suffix)
139}
140
141/// Get the time difference between a date and now as relative human readable string.
142///
143/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
144///
145/// # Arguments
146///
147/// * `datetime` - The NaiveDateTime to compare with the current time.
148/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
149/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
150///
151/// # Example
152///
153/// ```rust
154/// use chrono::DateTime;
155/// use ui2::utils::naive_format_distance_from_now;
156///
157/// fn time_since_first_moon_landing() -> String {
158///     let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
159///     format!("It's been {} since Apollo 11 first landed on the moon.", naive_format_distance_from_now(date, false, false))
160/// }
161/// ```
162///
163/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.`
164pub fn format_distance_from_now(
165    datetime: DateTimeType,
166    include_seconds: bool,
167    add_suffix: bool,
168) -> String {
169    let now = chrono::offset::Local::now().naive_local();
170
171    format_distance(datetime, now, include_seconds, add_suffix)
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use chrono::NaiveDateTime;
178
179    #[test]
180    fn test_naive_format_distance() {
181        let date = DateTimeType::Naive(
182            NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
183        );
184        let base_date = DateTimeType::Naive(
185            NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
186        );
187
188        assert_eq!(
189            "about 2 hours",
190            format_distance(date, base_date.to_naive(), false, false)
191        );
192    }
193
194    #[test]
195    fn test_naive_format_distance_with_suffix() {
196        let date = DateTimeType::Naive(
197            NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
198        );
199        let base_date = DateTimeType::Naive(
200            NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
201        );
202
203        assert_eq!(
204            "about 2 hours from now",
205            format_distance(date, base_date.to_naive(), false, true)
206        );
207    }
208
209    #[test]
210    fn test_naive_format_distance_from_now() {
211        let date = DateTimeType::Naive(
212            NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
213                .expect("Invalid NaiveDateTime for date"),
214        );
215
216        assert_eq!(
217            "over 54 years ago",
218            format_distance_from_now(date, false, true)
219        );
220    }
221
222    #[test]
223    fn test_naive_format_distance_string() {
224        assert_eq!(distance_string(3, false, false), "less than a minute");
225        assert_eq!(distance_string(7, false, false), "less than a minute");
226        assert_eq!(distance_string(13, false, false), "less than a minute");
227        assert_eq!(distance_string(21, false, false), "less than a minute");
228        assert_eq!(distance_string(45, false, false), "1 minute");
229        assert_eq!(distance_string(61, false, false), "1 minute");
230        assert_eq!(distance_string(1920, false, false), "32 minutes");
231        assert_eq!(distance_string(3902, false, false), "about 1 hour");
232        assert_eq!(distance_string(18002, false, false), "about 5 hours");
233        assert_eq!(distance_string(86470, false, false), "1 day");
234        assert_eq!(distance_string(345880, false, false), "4 days");
235        assert_eq!(distance_string(2764800, false, false), "about 1 month");
236        assert_eq!(distance_string(5184000, false, false), "about 2 months");
237        assert_eq!(distance_string(10368000, false, false), "4 months");
238        assert_eq!(distance_string(34694000, false, false), "about 1 year");
239        assert_eq!(distance_string(47310000, false, false), "over 1 year");
240        assert_eq!(distance_string(61503000, false, false), "almost 2 years");
241        assert_eq!(distance_string(160854000, false, false), "about 5 years");
242        assert_eq!(distance_string(236550000, false, false), "over 7 years");
243        assert_eq!(distance_string(249166000, false, false), "almost 8 years");
244    }
245
246    #[test]
247    fn test_naive_format_distance_string_include_seconds() {
248        assert_eq!(distance_string(3, true, false), "less than 5 seconds");
249        assert_eq!(distance_string(7, true, false), "less than 10 seconds");
250        assert_eq!(distance_string(13, true, false), "less than 20 seconds");
251        assert_eq!(distance_string(21, true, false), "half a minute");
252        assert_eq!(distance_string(45, true, false), "less than a minute");
253        assert_eq!(distance_string(61, true, false), "1 minute");
254    }
255}