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
22pub struct FormatDistance {
23 date: DateTimeType,
24 base_date: DateTimeType,
25 include_seconds: bool,
26 add_suffix: bool,
27 hide_prefix: bool,
28}
29
30impl FormatDistance {
31 pub fn new(date: DateTimeType, base_date: DateTimeType) -> Self {
32 Self {
33 date,
34 base_date,
35 include_seconds: false,
36 add_suffix: false,
37 hide_prefix: false,
38 }
39 }
40
41 pub fn from_now(date: DateTimeType) -> Self {
42 Self::new(date, DateTimeType::Local(Local::now()))
43 }
44
45 pub fn include_seconds(mut self, include_seconds: bool) -> Self {
46 self.include_seconds = include_seconds;
47 self
48 }
49
50 pub fn add_suffix(mut self, add_suffix: bool) -> Self {
51 self.add_suffix = add_suffix;
52 self
53 }
54
55 pub fn hide_prefix(mut self, hide_prefix: bool) -> Self {
56 self.hide_prefix = hide_prefix;
57 self
58 }
59}
60
61/// Calculates the distance in seconds between two NaiveDateTime objects.
62/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
63///
64/// ## Arguments
65///
66/// * `date` - A NaiveDateTime object representing the date of interest
67/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made
68fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
69 let duration = date.signed_duration_since(base_date);
70 -duration.num_seconds()
71}
72
73/// Generates a string describing the time distance between two dates in a human-readable way.
74fn distance_string(
75 distance: i64,
76 include_seconds: bool,
77 add_suffix: bool,
78 hide_prefix: bool,
79) -> String {
80 let suffix = if distance < 0 { " from now" } else { " ago" };
81
82 let distance = distance.abs();
83
84 let minutes = distance / 60;
85 let hours = distance / 3_600;
86 let days = distance / 86_400;
87 let months = distance / 2_592_000;
88
89 let string = if distance < 5 && include_seconds {
90 if hide_prefix {
91 "5 seconds"
92 } else {
93 "less than 5 seconds"
94 }
95 .to_string()
96 } else if distance < 10 && include_seconds {
97 if hide_prefix {
98 "10 seconds"
99 } else {
100 "less than 10 seconds"
101 }
102 .to_string()
103 } else if distance < 20 && include_seconds {
104 if hide_prefix {
105 "20 seconds"
106 } else {
107 "less than 20 seconds"
108 }
109 .to_string()
110 } else if distance < 40 && include_seconds {
111 if hide_prefix {
112 "half a minute"
113 } else {
114 "half a minute"
115 }
116 .to_string()
117 } else if distance < 60 && include_seconds {
118 if hide_prefix {
119 "a minute"
120 } else {
121 "less than a minute"
122 }
123 .to_string()
124 } else if distance < 90 && include_seconds {
125 "1 minute".to_string()
126 } else if distance < 30 {
127 if hide_prefix {
128 "a minute"
129 } else {
130 "less than a minute"
131 }
132 .to_string()
133 } else if distance < 90 {
134 "1 minute".to_string()
135 } else if distance < 2_700 {
136 format!("{} minutes", minutes)
137 } else if distance < 5_400 {
138 if hide_prefix {
139 "1 hour"
140 } else {
141 "about 1 hour"
142 }
143 .to_string()
144 } else if distance < 86_400 {
145 if hide_prefix {
146 format!("{} hours", hours)
147 } else {
148 format!("about {} hours", hours)
149 }
150 .to_string()
151 } else if distance < 172_800 {
152 "1 day".to_string()
153 } else if distance < 2_592_000 {
154 format!("{} days", days)
155 } else if distance < 5_184_000 {
156 if hide_prefix {
157 "1 month"
158 } else {
159 "about 1 month"
160 }
161 .to_string()
162 } else if distance < 7_776_000 {
163 if hide_prefix {
164 "2 months"
165 } else {
166 "about 2 months"
167 }
168 .to_string()
169 } else if distance < 31_540_000 {
170 format!("{} months", months)
171 } else if distance < 39_425_000 {
172 if hide_prefix {
173 "1 year"
174 } else {
175 "about 1 year"
176 }
177 .to_string()
178 } else if distance < 55_195_000 {
179 if hide_prefix { "1 year" } else { "over 1 year" }.to_string()
180 } else if distance < 63_080_000 {
181 if hide_prefix {
182 "2 years"
183 } else {
184 "almost 2 years"
185 }
186 .to_string()
187 } else {
188 let years = distance / 31_536_000;
189 let remaining_months = (distance % 31_536_000) / 2_592_000;
190
191 if remaining_months < 3 {
192 if hide_prefix {
193 format!("{} years", years)
194 } else {
195 format!("about {} years", years)
196 }
197 .to_string()
198 } else if remaining_months < 9 {
199 if hide_prefix {
200 format!("{} years", years)
201 } else {
202 format!("over {} years", years)
203 }
204 .to_string()
205 } else {
206 if hide_prefix {
207 format!("{} years", years + 1)
208 } else {
209 format!("almost {} years", years + 1)
210 }
211 .to_string()
212 }
213 };
214
215 if add_suffix {
216 format!("{}{}", string, suffix)
217 } else {
218 string
219 }
220}
221
222/// Get the time difference between two dates into a relative human readable string.
223///
224/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
225///
226/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now.
227///
228/// # Arguments
229///
230/// * `date` - The NaiveDateTime to compare.
231/// * `base_date` - The NaiveDateTime to compare against.
232/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
233/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
234///
235/// # Example
236///
237/// ```rust
238/// use chrono::DateTime;
239/// use ui2::utils::naive_format_distance;
240///
241/// fn time_between_moon_landings() -> String {
242/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
243/// let base_date = DateTime::parse_from_rfc3339("1972-12-14T00:00:00Z").unwrap().naive_local();
244/// format!("There was {} between the first and last crewed moon landings.", naive_format_distance(date, base_date, false, false))
245/// }
246/// ```
247///
248/// Output: `"There was about 3 years between the first and last crewed moon landings."`
249pub fn format_distance(
250 date: DateTimeType,
251 base_date: NaiveDateTime,
252 include_seconds: bool,
253 add_suffix: bool,
254 hide_prefix: bool,
255) -> String {
256 let distance = distance_in_seconds(date.to_naive(), base_date);
257
258 distance_string(distance, include_seconds, add_suffix, hide_prefix)
259}
260
261/// Get the time difference between a date and now as relative human readable string.
262///
263/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
264///
265/// # Arguments
266///
267/// * `datetime` - The NaiveDateTime to compare with the current time.
268/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
269/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
270///
271/// # Example
272///
273/// ```rust
274/// use chrono::DateTime;
275/// use ui2::utils::naive_format_distance_from_now;
276///
277/// fn time_since_first_moon_landing() -> String {
278/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
279/// format!("It's been {} since Apollo 11 first landed on the moon.", naive_format_distance_from_now(date, false, false))
280/// }
281/// ```
282///
283/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.`
284pub fn format_distance_from_now(
285 datetime: DateTimeType,
286 include_seconds: bool,
287 add_suffix: bool,
288 hide_prefix: bool,
289) -> String {
290 let now = chrono::offset::Local::now().naive_local();
291
292 format_distance(datetime, now, include_seconds, add_suffix, hide_prefix)
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use chrono::NaiveDateTime;
299
300 #[test]
301 fn test_format_distance() {
302 let date = DateTimeType::Naive(
303 NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
304 );
305 let base_date = DateTimeType::Naive(
306 NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
307 );
308
309 assert_eq!(
310 "about 2 hours",
311 format_distance(date, base_date.to_naive(), false, false, false)
312 );
313 }
314
315 #[test]
316 fn test_format_distance_with_suffix() {
317 let date = DateTimeType::Naive(
318 NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
319 );
320 let base_date = DateTimeType::Naive(
321 NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
322 );
323
324 assert_eq!(
325 "about 2 hours from now",
326 format_distance(date, base_date.to_naive(), false, true, false)
327 );
328 }
329
330 #[test]
331 fn test_format_distance_from_now() {
332 let date = DateTimeType::Naive(
333 NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
334 .expect("Invalid NaiveDateTime for date"),
335 );
336
337 assert_eq!(
338 "over 54 years ago",
339 format_distance_from_now(date, false, true, false)
340 );
341 }
342
343 #[test]
344 fn test_format_distance_string() {
345 assert_eq!(
346 distance_string(3, false, false, false),
347 "less than a minute"
348 );
349 assert_eq!(
350 distance_string(7, false, false, false),
351 "less than a minute"
352 );
353 assert_eq!(
354 distance_string(13, false, false, false),
355 "less than a minute"
356 );
357 assert_eq!(
358 distance_string(21, false, false, false),
359 "less than a minute"
360 );
361 assert_eq!(distance_string(45, false, false, false), "1 minute");
362 assert_eq!(distance_string(61, false, false, false), "1 minute");
363 assert_eq!(distance_string(1920, false, false, false), "32 minutes");
364 assert_eq!(distance_string(3902, false, false, false), "about 1 hour");
365 assert_eq!(distance_string(18002, false, false, false), "about 5 hours");
366 assert_eq!(distance_string(86470, false, false, false), "1 day");
367 assert_eq!(distance_string(345880, false, false, false), "4 days");
368 assert_eq!(
369 distance_string(2764800, false, false, false),
370 "about 1 month"
371 );
372 assert_eq!(
373 distance_string(5184000, false, false, false),
374 "about 2 months"
375 );
376 assert_eq!(distance_string(10368000, false, false, false), "4 months");
377 assert_eq!(
378 distance_string(34694000, false, false, false),
379 "about 1 year"
380 );
381 assert_eq!(
382 distance_string(47310000, false, false, false),
383 "over 1 year"
384 );
385 assert_eq!(
386 distance_string(61503000, false, false, false),
387 "almost 2 years"
388 );
389 assert_eq!(
390 distance_string(160854000, false, false, false),
391 "about 5 years"
392 );
393 assert_eq!(
394 distance_string(236550000, false, false, false),
395 "over 7 years"
396 );
397 assert_eq!(
398 distance_string(249166000, false, false, false),
399 "almost 8 years"
400 );
401 }
402
403 #[test]
404 fn test_format_distance_string_include_seconds() {
405 assert_eq!(
406 distance_string(3, true, false, false),
407 "less than 5 seconds"
408 );
409 assert_eq!(
410 distance_string(7, true, false, false),
411 "less than 10 seconds"
412 );
413 assert_eq!(
414 distance_string(13, true, false, false),
415 "less than 20 seconds"
416 );
417 assert_eq!(distance_string(21, true, false, false), "half a minute");
418 assert_eq!(
419 distance_string(45, true, false, false),
420 "less than a minute"
421 );
422 assert_eq!(distance_string(61, true, false, false), "1 minute");
423 }
424}