format_distance.rs

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