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}