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